stopclock
is a library for measuring time using (stop)clocks.
It allows you to create a clock, pause it and resume it. You can change its speed and adjust its time at any moment. For example you can have a clock that runs backwards 2 times faster than an ordinary clock.
You can load this library from quicklisp:
(ql:quickload :stopclock)
You can define local nickname for this package. (It is possible with almost all implementations, see Cookbook - packages - PLN). You can do it in your package definition:
(defpackage #:my-package
(:use #:cl)
(:local-nicknames (#:sc #:stopclock)))
Or during your REPL session:
CL-USER> (add-package-local-nickname '#:sc '#:stopclock)
; => #<PACKAGE "COMMON-LISP-USER">
The rest of this tutorial uses sc
as a local nickname for stopclock
.
You can run tests with asdf. Test system depends on fiveam
library.
(asdf:test-system :stopclock) ; depends on fiveam
; ...
; Did 564 checks.
; Pass: 564 (100%)
; Skip: 0 ( 0%)
; Fail: 0 ( 0%)
; => T
Note that you can’t (use-package :stopclock)
because it causes name-conflicts with common-lisp
package.
There are two function you will need to shadow: sc:time
and sc:speed
.
It is recommended to use package-local-nicknames
instead (see Local nickname).
To create a clock use make-clock
function.
CL-USER> (defparameter *c* (sc:make-clock))
; => *C*
CL-USER> *c*
; => #<CLOCK :TIME 1.98 SECONDS :RUNNING :SPEED x1>
You can stop the clock with stop
function and run it with run
function.
CL-USER> (sc:stop *c*)
; => #<CLOCK :TIME 11.08 SECONDS :PAUSED :SPEED x1>
CL-USER> (sc:run *c*)
; => #<CLOCK :TIME 11.08 SECONDS :RUNNING :SPEED x1>
CL-USER> *c*
; => #<CLOCK :TIME 15.70 SECONDS :RUNNING :SPEED x1> ; the clock is running again.
To get current clock time use time
function. It returns the time in seconds.
CL-USER> (sc:time *c*)
; => 12388023/500000 ; usually time is a rational number
CL-USER> (float (sc:time *c*))
; => 35.260098
Finally, you can reset the current time on the clock with reset
function.
CL-USER> (sc:reset *c*)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
It does not pause or run of the clock by default, but you can
specify it with :paused T
or :run T
key arguments.
CL-USER> (sc:reset *c* :paused T)
; => #<CLOCK :TIME 0.00 SECONDS :PAUSED :SPEED x1> ; clock is paused now
CL-USER> (sc:reset *c*)
; => #<CLOCK :TIME 0.00 SECONDS :PAUSED :SPEED x1> ; clock is still paused
CL-USER> (sc:reset *c* :run T)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1> ; clock is running now
Most functions, such as stop
and run
, act destructively on the clock
and return itself for convenience. You can copy the clock with copy-clock
.
CL-USER> (defparameter *c* (sc:make-clock))
; => *C*
CL-USER> (defparameter *d* (sc:copy-clock *c*))
; => *D*
CL-USER> (list *c* *d*)
; => (#<CLOCK :TIME 16.93 SECONDS :RUNNING :SPEED x1>
; #<CLOCK :TIME 16.93 SECONDS :RUNNING :SPEED x1>)
CL-USER> (list *c* (sc:stop *d*))
; => (#<CLOCK :TIME 28.90 SECONDS :RUNNING :SPEED x1>
; #<CLOCK :TIME 28.90 SECONDS :PAUSED :SPEED x1>)
CL-USER> (list *c* *d*)
; => (#<CLOCK :TIME 31.64 SECONDS :RUNNING :SPEED x1>
; #<CLOCK :TIME 28.90 SECONDS :PAUSED :SPEED x1>)
A clock has three parameters: time
, speed
and whether it is paused
or is running.
(speed
refers to the speed with which the time on the clock changes.)
You can pass these parameters to the initialization function. For example you can create a paused clock that runs backwards with 5 seconds in the beginning:
CL-USER> (sc:make-clock :paused t :time 5 :speed -1)
; => #<CLOCK :TIME 5.00 SECONDS :PAUSED :SPEED -x1>
CL-USER> (sc:run *)
; => #<CLOCK :TIME 5.00 SECONDS :RUNNING :SPEED -x1>
CL-USER> *
; => #<CLOCK :TIME 3.03 SECONDS :RUNNING :SPEED -x1>
For each of these parameters, a corresponding accessor is defined: time
, speed
, and paused
.
CL-USER> (setf (sc:paused *c*) t)
; => T
CL-USER> (setf (sc:speed *c*) -10)
; => -10
CL-USER> (list (float (sc:time *c*))
(sc:speed *c*)
(sc:paused *c*))
; => (322.43793 -10 T)
CL-USER> (setf (sc:time *c*) 100)
; => 100
CL-USER> *c*
; => #<CLOCK :TIME 100.00 SECONDS :PAUSED :SPEED -x10>
The paused / running state of the clock can be accessed with function paused
.
The state can be set by combining paused
with setf
.
It also can be set by functions run
(or a synonymous start
),
pause
(or a synonymous stop
) and toggle
.
These function return the clock itself.
CL-USER> (sc:make-clock :paused t)
; => #<CLOCK :TIME 0.00 SECONDS :PAUSED :SPEED x1>
CL-USER> (sc:run *) ; or (sc:start *)
; => #<CLOCK :TIME 0.04 SECONDS :RUNNING :SPEED x1>
CL-USER> (sc:stop *) ; or (sc:pause *)
; => #<CLOCK :TIME 4.47 SECONDS :PAUSED :SPEED x1>
CL-USER> (setf (sc:paused *) t)
; => T
CL-USER> **
; => #<CLOCK :TIME 4.47 SECONDS :PAUSED :SPEED x1>
The time on the clock can accessed with function time
.
You can set the time by combining time
with setf
.
There is also an adjust
function that adds a given number of seconds to the current clock time.
It is more efficient than using combination of incf
and time
.
Unlike setf
or incf
it returns the clock itself.
CL-USER> (sc:make-clock)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
CL-USER> (setf (sc:time (sc:stop *)) 0) ; stop returns the clock itself which allows chaining like that.
; => 0
CL-USER> **
; => #<CLOCK :TIME 0.00 SECONDS :PAUSED :SPEED x1>
CL-USER> (incf (sc:time *) 10)
; => 10
CL-USER> **
; => #<CLOCK :TIME 10.00 SECONDS :PAUSED :SPEED x1>
CL-USER> (sc:adjust * 20)
; => #<CLOCK :TIME 30.00 SECONDS :PAUSED :SPEED x1>
The speed of the clock can accessed with speed
.
You can set it by combining speed
with setf
.
There is also an accelerate
function that will multiply the speed by a given factor.
Unlike setf
or incf
it returns the clock itself.
CL-USER> (sc:make-clock)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
CL-USER> (setf (sc:speed *) 10)
; => 10
CL-USER> **
; => #<CLOCK :TIME 26.72 SECONDS :RUNNING :SPEED x10>
CL-USER> (setf (sc:speed *) -100)
; => -100
CL-USER> **
; => #<CLOCK :TIME -39.91 SECONDS :RUNNING :SPEED -x100>
CL-USER> (sc:accelerate * -2)
; => #<CLOCK :TIME -1020.11 SECONDS :RUNNING :SPEED x200>
CL-USER> *
; => #<CLOCK :TIME 1995.27 SECONDS :RUNNING :SPEED x200>
The speed of the clock cannot be equal to zero.
If you try to set it to zero the zero-clock-speed-error
will be signalled.
CL-USER> (sc:make-clock :speed 0)
; Evaluation aborted on #<SC:ZERO-CLOCK-SPEED-ERROR {1006E41983}>.
To reset the clock you can use reset
function.
By default it only resets the time to 0.
You can pass one of :paused
or :run
key arguments to
set the clock’s state to the corresponding value.
You can also specify :speed
and :time
to be set.
The function returns the clock itself.
CL-USER> (sc:make-clock)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
CL-USER> (sc:reset * :paused t)
; => #<CLOCK :TIME 0.00 SECONDS :PAUSED :SPEED x1>
CL-USER> (sc:reset * :run t)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
CL-USER> (sc:reset * :speed 10)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x10>
CL-USER> (sc:reset * :time -10)
; => #<CLOCK :TIME -10.00 SECONDS :RUNNING :SPEED x10>
The :paused
arguments takes precedence over :run
:
CL-USER> (sc:reset *c* :paused t :run t)
; => #<CLOCK :TIME 0.00 SECONDS :PAUSED :SPEED x10>
By default the clock will get current time with get-internal-real-time
function.
This behaviour can be changed by passing :time-source
parameter to the make-clock
function.
This must be a function that returns the current time in seconds.
(It also can be another clock, see Synchronized clocks.)
stopclock
defines two possible time-sources:
real-time
that uses get-internal-real-time
is used by default,
and run-time
that uses get-internal-run-time
instead.
CL-USER> (let ((real-clock (sc:make-clock :paused nil :time-source 'sc:real-time)) ; default time source
(run-clock (sc:make-clock :paused nil :time-source 'sc:run-time)))
(sleep 5)
(list real-clock run-clock))
; => (#<CLOCK :TIME 5.00 SECONDS :RUNNING :SPEED x1>
; #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>)
It is impossible to start or stop two clocks at the same time, since they may have different time sources. However, synchronized clocks can be obtained by using a third clock as the time source. Consider this example:
CL-USER> (let ((1x (sc:make-clock))
(latency (sleep 0.01))
(5x (sc:make-clock :speed 5)))
(declare (ignore latency))
(sleep 1)
(= (* 5 (sc:time 1x))
(sc:time 5x)))
; => NIL
We create two clocks, one running 5 times faster than another. We also introduce an artificial latency between their creation. As a result they are out of sync. If we use the third clock as the time source paused during the creation of clocks, then the clocks are synchronized:
CL-USER> (let* ((clock (sc:make-clock :paused t))
(1x (sc:make-clock :time-source (lambda () (sc:time clock))))
(latency (sleep 0.01))
(5x (sc:make-clock :time-source (lambda () (sc:time clock))
:speed 5)))
(declare (ignore latency))
(sc:run clock)
(sleep 1)
(sc:stop clock)
(= (* 5 (sc:time 1x))
(sc:time 5x)))
; => T
For convenience you can directly pass another clock as the time source. Here is another example:
CL-USER> (let* ((source-clock (sc:make-clock :paused t))
(up (sc:make-clock :time-source source-clock))
(down (sc:make-clock :time-source source-clock
:speed -1 :time 50)))
(sc:run source-clock)
(format t "; up: ~a~%; down: ~a~%" up down)
(sleep 1)
(format t "; up: ~a~%; down: ~a~%" up down)
(sc:stop source-clock)
(= 50 (+ (sc:time up) (sc:time down))))
; up: #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
; down: #<CLOCK :TIME 50.00 SECONDS :RUNNING :SPEED -x1>
; up: #<CLOCK :TIME 1.00 SECONDS :RUNNING :SPEED x1>
; down: #<CLOCK :TIME 49.00 SECONDS :RUNNING :SPEED -x1>
; => T
Time on the clocks up
and down
will always add up to 50.
If you want to read the time on synchronized clocks you need to pause the common source clock first.
That means that the time spent on processing time values will not be tracked.
Clock freeze solves this problem.
When you freeze
the clock it freezes the time on the clock, which is almost identical to pausing it.
However, when you unfreeze
it, the clock behaves as if it had not been frozen.
CL-USER> (sc:make-clock)
; => #<CLOCK :TIME 0.00 SECONDS :RUNNING :SPEED x1>
CL-USER> (sc:freeze *)
; => #<CLOCK :TIME 4.19 SECONDS :FREEZED :SPEED x1>
CL-USER> *
; => #<CLOCK :TIME 4.19 SECONDS :FREEZED :SPEED x1>
CL-USER> (sc:unfreeze *)
; => #<CLOCK :TIME 10.36 SECONDS :RUNNING :SPEED x1> ; about 6 seconds elapsed during the freeze.
It also means that the paused clock will remain paused after the freeze.
CL-USER> (sc:make-clock :time 3 :paused t)
; => #<CLOCK :TIME 3.00 SECONDS :PAUSED :SPEED x1>
CL-USER> (sc:freeze *)
; => #<CLOCK :TIME 3.00 SECONDS :PAUSED :SPEED x1>
CL-USER> (sc:unfreeze *)
; => #<CLOCK :TIME 3.00 SECONDS :PAUSED :SPEED x1>
CL-USER> *
; => #<CLOCK :TIME 3.00 SECONDS :PAUSED :SPEED x1>
stopclock
also provides a macro with-freeze
. Consider the previous example:
CL-USER> (let* ((source-clock (sc:make-clock :paused t))
(up (sc:make-clock :time-source source-clock))
(down (sc:make-clock :time-source source-clock
:speed -1 :time 50)))
(sc:run source-clock)
(loop repeat 5
do (sleep 0.1)
always (= 50 (sc:with-freeze source-clock
(+ (sc:time up) (sc:time down))))))
; => T
To keep the time read from up
and down
clocks in sync,
we freeze their common source each time we need to read them.
Feel free to report bugs or make suggestions by filing an issue on github.
Feel free to submit pull requests on github as well.
Copyright 2023 Gleefre
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.