Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit of spyscope

  • Loading branch information...
commit 4479ffad5b30164b17bf3431f02d122a6d99b636 0 parents
David Greenberg authored
10 .gitignore
@@ -0,0 +1,10 @@
+/target
+/lib
+/classes
+/checkouts
+pom.xml
+*.jar
+*.class
+.lein-deps-sum
+.lein-failures
+.lein-plugins
111 README.md
@@ -0,0 +1,111 @@
+# Spyscope
+
+A Clojure library designed to make it easy to debug single- and multi-threaded applications.
+
+## Usage
+
+Include `[spyscope "0.0.1"]` in your project.clj's `:dependencies`.
+
+Spyscope includes 3 reader tools for debugging your Clojure code, which are exposed as reader tags:
+`#spy/p`, `#spy/d`, and `#spy/t`, which stand for *print*, *details*, and *trace*, respectively.
+Reader tags were chosen because they allow one to use Spyscope by only writing 6 characters, and
+since they exist only to the left of the form one wants to debug, they require the fewest possible
+keystrokes, optimizing for developer happiness. :)
+
+### `#spy/p`
+
+First, let's look at `#spy/p`, which just pretty-prints the form of interest:
+
+ spyscope.repl=> (take 20 (repeat #spy/p (+ 1 2 3)))
+ 6
+ (6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)
+
+`#spy/p` is an extremely simple tool that merely saves a few keystores when
+one needs to dump out a value in the middle of a calculation.
+
+### `#spy/d`
+
+Next, let's look at `#spy/d`. This is where the real power lies:
+
+ spyscope.repl=> (take 20 (repeat #spy/d (+ 1 2 3)))
+ spyscope.repl$eval672.invoke(REPL:12) => 6
+ (6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)
+
+In the simplest usage, the form is printed along with the stack trace
+it occurred on, which makes it easier to grep through logs that have
+many tracing statements enabled.
+
+Often, you may find that additional context would be beneficial, so
+you can request additional stack frames with the metadata key `:fs`
+(for *f*rame*s*):
+
+ spyscope.repl=> (take 20 (repeat #spy/d ^{:fs 3} (+ 1 2 3)))
+ ----------------------------------------
+ clojure.lang.Compiler.eval(Compiler.java:6477)
+ clojure.lang.Compiler.eval(Compiler.java:6511)
+ spyscope.repl$eval675.invoke(REPL:13) => 6
+ (6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)
+
+As you can see, when multiple stack frames are printed, a row of dashes
+is printed before the trace to keep the start of the stack frame group
+clearly denoted.
+
+As you debug further, you may realize that the context of the creation of
+certain values is important; however, if you print out 10 or 20 lines of
+stack trace, you'll end up with an unreadable mess. The metadata key `:nses`
+allows you to apply a regex to the stacktrace frames to filter out noise:
+
+ spyscope.repl=> (take 20 (repeat #spy/d ^{:fs 3 :nses #"core|spyscope"} (+ 1 2 3)))
+ ----------------------------------------
+ clojure.core$apply.invoke(core.clj:601)
+ clojure.core$eval.invoke(core.clj:2797)
+ spyscope.repl$eval678.invoke(REPL:14) => 6
+ (6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6)
+
+The last feature of `#spy/d` is that it can print the code that generated
+the value, which can help you disambiguate multiple nearby related values.
+This is controlled by setting the metadata key `:ast` to `true`:
+
+ spyscope.repl=> {:a #spy/d ^{:ast true} (+ 1 2 3)
+ :b #spy/d ^{:ast true} (- 16 10)}
+ spyscope.repl$eval685.invoke(REPL:16) (+ 1 2 3) => 6
+ spyscope.repl$eval685.invoke(REPL:16) (- 16 10) => 6
+ {:a 6, :b 6}
+
+### `spy/t`
+
+Finally, let's look at `#spy/t`. Tracing is very similar to detailed
+printing, but it enables us to get meaningful results when using `#spy/d`
+on a program that has multiple interacting threads without affecting
+most interactive development workflows!
+
+`#spy/t` accepts all of the metadata arguments that `#spy/d` does (i.e.
+`:fs`, `:nses`, and `:ast`).
+
+Instead of immediately printing out results, it stores them in an
+agent asynchronously. Each time a trace is logged, it is placed into
+the current generation. One can use a function to increment the generation
+counter, and previous generations are stored, so that one can compare
+several recent generations to understand what effects changes may have had.
+
+There are several functions you can use to interact with the trace store:
+
+`trace-query` is the workhorse function. With no arguments, it prints every
+trace from the current generation. With a numeric argument `generations`,
+it prints every trace from the past `generations` generations. With a
+regex argument `re`, it prints every trace from the current generation whose
+root stack frame matches the regex. Also accepts 2 arguments to specify the
+filtering regex and how many generations to include.
+
+`trace-next` moves onto the next generation. One usually calls this between
+trials or experiments.
+
+`trace-clear` deletes all trace data collected so far. Since all trace
+data is saved, that can become quite a lot of data, so this can be used
+to clean up very long running sessions.
+
+## License
+
+Copyright © 2012 David Greenberg
+
+Distributed under the Eclipse Public License, the same as Clojure.
6 project.clj
@@ -0,0 +1,6 @@
+(defproject spyscope "0.1.0-SNAPSHOT"
+ :description "FIXME: write description"
+ :url "http://example.com/FIXME"
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}
+ :dependencies [[org.clojure/clojure "1.4.0"]])
3  src/data_readers.clj
@@ -0,0 +1,3 @@
+{spy/p spyscope.core/print-log
+ spy/d spyscope.core/print-log-detailed
+ spy/t spyscope.core/trace}
99 src/spyscope/core.clj
@@ -0,0 +1,99 @@
+(ns spyscope.core
+ "This co"
+ (require [clojure.pprint :as pp]
+ [clojure.string :as str]))
+
+(defn- indent
+ "Indents a string with `n` spaces."
+ [n string]
+ (let [indent (str/join (repeat n " "))]
+ (->> (str/split string #"\n")
+ (map (partial str indent))
+ (str/join "\n"))))
+
+(defn pretty-render-value
+ "Prints out a form and some extra info for tracing/debugging.
+
+ Prints the last `n` stack frames"
+ [form meta]
+ (let [nses-regex (:nses meta)
+ n (or (:fs meta) 1)
+ frames-base (->> (ex-info "" {})
+ .getStackTrace
+ seq
+ (drop 2))
+ frames (if nses-regex
+ (filter (comp (partial re-find nses-regex) str)
+ frames-base)
+ frames-base)
+ frames (->> frames
+ (take n)
+ (map str)
+ (reverse))
+
+
+ w (java.io.StringWriter.)
+ _ (pp/pprint form w)
+ value-string (str w)
+
+ ;Strip trailing line break
+ value-string (.substring value-string 0 (dec (count value-string)))
+
+ ;Are there multiple trace lines?
+ multi-trace? (> n 1)
+
+ ;Indent if it's a multi-line structure
+ value-string (if (or (> (count value-string) 40)
+ (.contains value-string "\n"))
+ (str "\n" (indent 2 value-string))
+ value-string)
+
+ prefix (str/join "\n" frames)]
+ {:message (str
+ (when multi-trace?
+ (str (str/join (repeat 40 \-)) \newline))
+ prefix
+ (when (:ast meta)
+ (str " " (pr-str (::form meta))))
+ " => " value-string)
+ :frame1 (str (first frames-base))}))
+
+(defn print-log-detailed
+ "Reader function to pprint a form's value with some extra information."
+ [form]
+ (let [{:keys [fs nses]} (meta form)]
+ `(let [f# ~form]
+ (println (:message (pretty-render-value f# ~(assoc (meta form)
+ ::form (list 'quote form)))))
+ f#)))
+
+(defn print-log
+ "Reader function to pprint a form's value."
+ [form]
+ `(doto ~form pp/pprint))
+
+(def ^{:internal true} trace-storage (agent {:trace [] :generation 0}))
+
+(defn trace
+ "Reader function to store detailed information about a form's value at runtime
+ into a trace that can be queried asynchronously."
+ [form]
+ `(let [f# ~form]
+ (send trace-storage
+ (fn [{g# :generation t# :trace :as storage#}]
+ (assoc storage#
+ :trace
+ (conj t# (assoc (pretty-render-value
+ f#
+ ~(assoc (meta form)
+ ::form (list 'quote form)))
+ :generation g#)))))
+ f#))
+
+(defn fib
+ "Fibonacci number generator--an experimental tracing candidate"
+ ([x]
+ (fib (dec x) x))
+ ([n x]
+ (if (zero? n) x (fib #spy/t ^{:ast true} (dec n)
+ #spy/t ^{:ast true} (* n x)))))
47 src/spyscope/repl.clj
@@ -0,0 +1,47 @@
+(ns spyscope.repl
+ "This contains the query functions suitable for inspecting traces
+ from the repl."
+ (require [clojure.string :as str])
+ (:use [spyscope.core :only [trace-storage]]))
+
+(defn trace-query
+ "Prints information about trace results.
+
+ With no arguments, this prints every trace from the current generation.
+
+ With one numeric argument `generations`, this prints every trace from the previous
+ `generations` generations.
+
+ With one regex argument `re`, this prints every trace from the current generation
+ whose first stack frame matches the regex.
+
+ With two arguments, `re` and `generations`, this matches every trace whose stack frame
+ matches `re` from the previosu `generations` generations."
+ ([]
+ (trace-query #".*" 1))
+ ([re-or-generations]
+ (if (number? re-or-generations)
+ (trace-query #".*" re-or-generations)
+ (trace-query re-or-generations 1)))
+ ([re generations]
+ (let [{:keys [generation trace]} @trace-storage
+ generation-min (- generation generations)]
+ (->> trace
+ (filter #(re-find re (:frame1 %)))
+ (filter #(> (:generation %) generation-min))
+ (map :message)
+ (interpose (str/join (repeat 40 "-")))
+ (str/join "\n")
+ (println)))))
+
+(defn trace-next
+ "Increments the generation of future traces."
+ []
+ (send trace-storage update-in [:generation] inc)
+ nil)
+
+(defn trace-clear
+ "Deletes all trace data so far (used to reduce memory consumption)"
+ []
+ (send trace-storage (fn [_] {:trace [] :generation 0}))
+ nil)
Please sign in to comment.
Something went wrong with that request. Please try again.