demo-docs-tools.trace
I have programmed using Clojure as a hobby for several years, and knew of tools.trace, but other than trying it out once or twice, never used it much. When using it for debugging an issue, I ran across some confusing aspects of the included documentation, and also perhaps some limitations of what the library can trace, vs. what it cannot, and I wanted to make some notes about them for myself.
Usage
See forms to evaluate in comment block at end of src/demo1/ns1.clj
alter-var-root
tools.trace and spec work by using At least this is true for Clojure/JVM (Clojure running on the JVM). I haven't checked the details on ClojureScript.
alter-var-root
based tools work, vs. not
Cases where Here are Clojure tools that work by using alter-var-root
:
trace-ns
andtrace-vars
in thetools.trace
libraryinstrument
in Clojure spec
These tools, that might be in some ways similar to the above, do not
use alter-var-root
:
memo
and similar functions in thecore.memoize
library, as well asmemoize
built into Clojure. These take a function as an argument and return a different function based on the one you give them. They do not modify the behavior of any existing functions.
Here is a summary of where in Clojure/JVM based programs you can
expect tools based on alter-var-root
to work, vs. where they will
not work.
- They work on Clojure functions defined with
(defn foo ...)
or(def foo (fn ...))
.- Exception: calls made in code compiled with direct linking enabled. Starting with Clojure 1.8.0, all code in the core of Clojure is compiled with direct linking enabled, so calls within the core Clojure implementation to other functions within the core Clojure implementation are direct linked, and always use the original function definition.
- Exception: calls where the compiler has inlined the function definition.
- Exception: primitive type hinted functions (mentioned by Alex Miller - TBD an example of this to test?)
- On a Clojure protocol method defined with
(defprotocol MyProtocol (my-method ...))
- But only if the definition of
my-method
is given usingextend
,extend-type
, orextend-protocol
. - Exception: If the definition of
my-method
is given inside of adeftype
ordefrecord
form, it will not be affected.
- But only if the definition of
In addition to the exceptions listed above, they do not work for these things:
- Direct Java method calls using Clojure's Java interop syntax,
e.g.
(.javaMethodName ...)
or(java.contructorName. ...)
.
TBD: What about multimethods?
alter-var-root
work
Conditions where tools based on In Clojure/JVM, when you do defn
, a Clojure Var is created, and its
value becomes the value of the function object created. When you
compile your Clojure code with direct linking disabled (which is the
default setting), every time you call such a function created via
defn
by its name, the compiled code generated for the call will look
up the current value of the Clojure Var with that name, and whatever
that value is, that is the function that is called.
The disadvantage of this level of indirection is a small additional
execution time on each function call. The advantage is that it is
straightforward to use defn
while a JVM is running, e.g. via a REPL
to that JVM, to change the value of a function. Any future calls to
that function will use the new definition rather than the old one.
Direct linking was created as an option in the Clojure/JVM compiler to give another choice. The extra level of indirection is not included where functions are called. The value of the Var is looked up at compile time, and a reference to the compiled function is used directly in the function call. The advantage is a slightly faster function call. The disadvantage is that if you redefine the value of the Var, any calls to that Var compiled with direct linking will continue using the old definition, not the new one.
The tools.trace
library can (in many cases) enable trace messages to
be printed when a function is called and when it returns, including
printing the values of arguments at call time and the return value at
return time. The tools.trace
library's trace-vars
and trace-ns
functions work by modifying the value of Clojure Vars. For example:
user=> (require '[clojure.tools.trace :as t])
nil
user=> (defn f1 [x]
(inc x))
#'user/f1
user=> (defn f2 [y]
(- (f1 y) 7))
#'user/f2
user=> (f2 10)
4
user=> (t/trace-vars f1 f2)
#'user/f2
user=> (f2 10)
TRACE t1775: (user/f2 10)
TRACE t1776: | (user/f1 10)
TRACE t1776: | => 11
TRACE t1775: => 4
4
user=> (t/untrace-vars f1 f2)
#'user/f2
How does it do this? When you call trace-vars
, it calls a Clojure
core function
alter-var-root
(ClojureDocs.org
page), which
modifies the value of a Var, to assign new values to the Vars. The
new value assigned to the Var #'user/f1
is a "wrapping" version of
the original function. This "wrapping" function performs these steps:
- Print a trace message showing the name
user/f1
and the values of the parameters. - Call the original function.
- Print a trace message showing the name and the return value.
- Return the value that the original function did.
Now any call to user/f1
that performs the one level of indirection
through the Var #'user/f1
will call this modified function instead
of the original.
Most of the work of trace-vars
is done inside of a function called
trace-var*
.
You can see the call to alter-var-root
here.
In order for tools.trace
's function untrace-vars
to be able to
disable tracing on a function, trace-vars
also remembers the value
of the original function in a place where untrace-vars
can find it.
trace-var*
stores the original function value in the metdata of the
Var with key :clojure.tools.trace/traced
here,
which is what ::traced
expands into when read by Clojure in the
namespace clojure.tools.trace
.
The function untrace-var*
in tools.trace
uses alter-var-root
to
restore the original value of the Var, and remove the saved function
from the Var's metadata. You can see those steps
here.
Clojure spec's instrument
and unstrument
functions work similarly.
Here
is the part of instrument
that replaces the original function with a
wrapping version that contains the additional instrumenting code, and
here
is the part of unstrument
that restores the Var back to its original
value.
License
Copyright © 2018 Andy Fingerhut
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.