This presentation is about instrumenting Clojure code, that is how one goes about modifying an existing code base to monitor or evaluate its performance or behavior.
It looks at alter-var-root
, a function of the Clojure runtime library
that provides a canonical approach to instrumentation and it looks at
figleaf, a Clojure library that provides a general purpose
instrumentation library and an implementation of a code coverage
facility.
In computer programming instrumentation is the activity of modifying an existing code base to monitor or evaluate its performance or behavior.
Some of the use cases for instrumentation include:
- code coverage
- profiling
- logging
- other forms of statistical analysis
Code coverage, profiling and logging are some of the more well known use cases for instrumentation. It is possible to imagine others though. For example, a probe might be placed on a withdraw payment function in a banking application to observe and record unusually large withdrawals.
- Instrument at compile time
- Requires special builds
- Requires semantic knowledge of the language (parser technology)
- Instrument at run-time or class-load time
- Special build not required
- Instrument live images
- Remove instrumentation from live images
- Instrumentation simple for Clojure and other languages where functions are first-class objects
john-popletts-computer:figleaf john$ lein repl
REPL started; server listening on localhost port 18864
user=> (def baz (fn [n] (* n n)))
#'user/baz
user=> (baz 8)
64
user=> baz
#<user$baz user$baz@1d417690>
user=> (def baz
(let [oldfn baz]
(fn [n] (do
(println "Hello baz!")
(oldfn n)))))
#'user/baz
user=> (baz 8)
Hello baz!
64
The REPL exercise above is not ideal. While it works, it is not atomic
or thread-safe. The canonical method is to use the alter-var-root
function.
To instrument a function, you need to take the body of an existing function, modify it in some way, and rebind the var for that function to the new modified function.
Here is almost the same example as above, taken from ClojureDocs,
using alter-var-root
:
(defn sqr [n]
"Squares a number"
(* n n))
user=> (sqr 5)
25
user=> (alter-var-root
(var sqr) ; var to alter
(fn [f] ; fn to apply to the var's value
(do (println "Squaring" %) ; returns a new fn wrapping old fn
(f %))))
user=> (sqr 5)
Squaring 5
25
- Instrumentation library
- Minimalist code-coverage library layered on top
- Derived from cl-figleaf, its Common Lisp predecessor
With alt-var-root
we have the rudimentary support we
require to develop an instrumentation package. What are some candidate
requirements?
- Instrument a name space in one go
- Unwind the instrumentation after we’re done
(defn instrument-function [var-name pre post]
(do
(alter-var-root var-name
(fn [function]
(with-meta
(fn [& args]
(if pre (pre (str var-name) args))
(let [result (apply function args)]
(if post (post (str var-name) args))
result))
(assoc (meta function)
:figleaf/original function))))
#(alter-var-root var-name (fn [function] (:figleaf/original (meta function))))))
- Commonly begin with a
with-
prefix (e.g. CL’swith-open-file
or Clojure’swith-open
) - Acquire a resource, set a condition or bind a var
- Execute a form
- Restore resource, original condition or value of a var
- Guarantee restore even when bad things happen with
unwind-protect
(CL) ortry/finally
(Clojure) - See section on Wrapper Macro’s in Programming Clojure for more
Figleaf implements with-instrument-namespace
, a macro that takes a
namespace and optional methods that are invoked before and after
functions in the library.
with-instrument-namespace [ns pre post body]
In the tradition of with-
macros, it instruments each public
function in the namespace with the pre and post functions, executes
the forms reprsented by “body” and then removes the instrumentation
before returning.
A lot of work is performed with high-order Clojure functions by the
relatively terse instrument-namespace
function:
(defn instrument-namespace [namespace-under-test pre post]
"Instrument a namespace. Wrap in docall is necessary to make sure call methods are instrumented
ahead of use."
(doall
(map
#(instrument-function %1 pre post)
(filter standard-fn?
(vals
(ns-publics namespace-under-test))))))
(defn with-instrument-namespace-fn [ns pre post body]
(let [restore-list (instrument-namespace ns pre post)
restore #(doseq [restore-fn restore-list]
(restore-fn))]
(try (body)
(finally (restore)))))
(defmacro with-instrument-namespace [ns pre post & body]
`(with-instrument-namespace-fn
'~ns ~pre ~post (fn [] ~@body)))
Figleaf provides a run-tests method that instruments a namespace and executes unit tests on it. The current implementation assumes a one-to-one relationship between regular and unit test namespaces.
run-tests [namespace-under-test unit-test-namespace]
(defmacro run-tests [namespace-under-test unit-test-namespace]
`(do
(with-instrument-namespace ~namespace-under-test increment-funcall-count nil
(test/run-tests '~unit-test-namespace))
(printf "CODE COVERAGE: Functions %d, Tested %d, Ratio %2.0f%%\n" (namespace-function-count)
(tested-function-count) (/ (tested-function-count) (namespace-function-count) 0.01))))
Figleaf also implements a Leiningen plugin “lein-figleaf” that implements a “figleaf” task that runs figleaf code coverage in the context of a Leiningen project.
To install the plugin:
lein plugin install lein-figleaf 1.0.1-SNAPSHOT
lein figleaf html-template html-template.test
Testing html-template.test
Ran 7 tests containing 13 assertions.
0 failures, 0 errors.
CODE COVERAGE: Functions 27, Tested 23, Ratio 85%
- Try / catch recursion constraint
instrument-namespace
doall fix- Backtick / quoting mischief
The source code for Figleaf is located on Github: https://github.com/John-Poplett/figleaf.
- alt-var-root, Clojure docs on
alt-var-root
- figleaf, the figleaf library
- Radagast, Phil Hagelberg’s code coverage library
- Robert Hooke, Phil Hagelberg’s instrumentation library