Skip to content

Commit

Permalink
Add trace facilities (#65)
Browse files Browse the repository at this point in the history
* Add trace util

* Fix Trace PNG

* Update copyright
  • Loading branch information
camsaul authored Jun 11, 2021
1 parent 194c615 commit ea67b8a
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 37 deletions.
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# Methodical

![Methodical](https://github.com/camsaul/methodical/blob/master/assets/logo.png)
![Methodical](assets/logo.png)

Methodical is a library that provides drop-in replacements for Clojure multimethods and adds several advanced features.

Expand Down Expand Up @@ -171,7 +171,7 @@ the next method. `:around` methods are invoked from least-specific to most-speci
(m/defmethod around-example :default
[x acc]
(conj acc :default))

(around-example {:type String} [])
;; -> [:object-before :string-before :default :string-after :object-after]
```
Expand Down Expand Up @@ -295,8 +295,8 @@ them:

* The *method combination*, which defines the way applicable primary and auxiliary methods are combined into a single
*effective method*. The default method combination, `thread-last-method-combination`, binds implicit `next-method`
args for primary and `:around` methods, and implements logic to thread the result of each method into the last argument of the next. Method combinations also specify which auxiliary method *qualifiers* (e.g. `:before` or `:around`) are
allowed, and how `defmethod` macro forms using those qualifiers are expanded (e.g., whether they get an implicit
args for primary and `:around` methods, and implements logic to thread the result of each method into the last argument of the next. Method combinations also specify which auxiliary method *qualifiers* (e.g. `:before` or `:around`) are
allowed, and how `defmethod` macro forms using those qualifiers are expanded (e.g., whether they get an implicit
`next-method` arg). Method combinations implement the `MethodCombination` interface.

* The *method table* stores primary and auxiliary methods, and returns them when asked. The default implementation,
Expand Down Expand Up @@ -478,10 +478,18 @@ following summarizes all component implementations that currently ship with Meth

* `cached-multifn-impl` -- wraps another multifn impl and an instance of `Cache` to implement caching.

### Debugging

Methodical offers debugging facilities so you can see what's going on under the hood, such as the `trace` utility
added in 0.11.4:

![Trace](assets/tracing.png)

## Performance

Methodical is built with performance in mind. Although it is written entirely in Clojure, and supports many more
features, its performance is similar or better to vanilla Clojure multimethods in many cases. Profiling results with [Criterium](https://github.com/hugoduncan/criterium/) show Methodical performing up to 20% faster in some cases:
features, its performance is similar or better to vanilla Clojure multimethods in many cases. Profiling results with
[Criterium](https://github.com/hugoduncan/criterium/) show Methodical performing up to 20% faster in some cases:

```
;;; Vanilla clojure
Expand All @@ -506,7 +514,7 @@ There is still room for even more performance improvement!
## License
Code, documentation, and artwork copyright © 2019 Cam Saul.
Code, documentation, and artwork copyright © 2019-2021 Cam Saul.
Distributed under the [Eclipse Public
License](https://raw.githubusercontent.com/metabase/camsaul/methodical/LICENSE.txt), same as Clojure.
Binary file added assets/tracing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
["docstring-checker"]]}

:dependencies
[[pretty "1.0.5"]
[[mvxcvi/puget "1.3.1"]
[pretty "1.0.5"]
[potemkin "0.4.5"]]

:profiles
Expand Down
13 changes: 11 additions & 2 deletions src/methodical/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
prefer-method
prefers])
(:require [methodical impl interface macros util]
methodical.util.trace
[potemkin :as p]))

;; fool cljr-clean-ns and the namespace linter so it doesn't remove these automatically
(comment methodical.macros/keep-me methodical.impl/keep-me methodical.interface/keep-me methodical.util/keep-me)
(comment
methodical.impl/keep-me
methodical.interface/keep-me
methodical.macros/keep-me
methodical.util.trace/keep-me
methodical.util/keep-me)

(p/import-vars
[methodical.macros
Expand Down Expand Up @@ -110,4 +116,7 @@
add-aux-method-with-unique-key!
remove-aux-method-with-unique-key!
remove-all-methods!
prefer-method!])
prefer-method!]

[methodical.util.trace
trace])
4 changes: 2 additions & 2 deletions src/methodical/impl/combo/common.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
(when (seq primary-methods)
(reduce
(fn [next-method primary-method]
(partial primary-method next-method))
(with-meta (partial primary-method next-method) (meta primary-method)))
nil
(reverse primary-methods))))

Expand All @@ -19,7 +19,7 @@
[combined-method around-methods]
(reduce
(fn [combined-method around-method]
(partial around-method combined-method))
(with-meta (partial around-method combined-method) (meta around-method)))
combined-method
around-methods))

Expand Down
16 changes: 8 additions & 8 deletions src/methodical/impl/combo/threaded.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
(ns methodical.impl.combo.threaded
(:refer-clojure :exclude [methods])
(:require [methodical.impl.combo.common :as combo.common]
methodical.interface
[potemkin.types :as p.types]
[pretty.core :refer [PrettyPrintable]])
(:import methodical.interface.MethodCombination))
Expand Down Expand Up @@ -30,13 +29,14 @@
threaded-fn (combine-with-threader threader methods)
optimized-one-arg-fn (apply comp (reverse methods))]
(combo.common/apply-around-methods
(fn
([] (optimized-one-arg-fn))
([a] (optimized-one-arg-fn a))
([a b] (threaded-fn a b))
([a b c] (threaded-fn a b c))
([a b c d] (threaded-fn a b c d))
([a b c d & more] (apply threaded-fn a b c d more)))
(-> (fn
([] (optimized-one-arg-fn))
([a] (optimized-one-arg-fn a))
([a b] (threaded-fn a b))
([a b c] (threaded-fn a b c))
([a b c d] (threaded-fn a b c d))
([a b c d & more] (apply threaded-fn a b c d more)))
(vary-meta assoc :methodical/combined-method? true))
around)))))

(defmulti threading-invoker
Expand Down
37 changes: 19 additions & 18 deletions src/methodical/util.clj
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
(keys (i/aux-methods multifn))))

(defn remove-aux-method-with-unique-key
"Remove an auxiliary method that was added by `add-aux-method-with-unique-key`, if one exists. Returns multifn."
"Remove an auxiliary method that was added by [[add-aux-method-with-unique-key]], if one exists. Returns `multifn`."
[multifn qualifier dispatch-val unique-key]
{:pre [(some? multifn)]}
(if-let [method (some
Expand All @@ -162,7 +162,7 @@

(defn add-aux-method-with-unique-key
"Adds an auxiliary method with a `unique-key` stored in its metadata. This unique key can later be used to remove the
auxiliary method with `remove-aux-method-with-unique-key`. If a method with this key already exists for this
auxiliary method with [[remove-aux-method-with-unique-key]]. If a method with this key already exists for this
qualifier and dispatch value, replaces the original."
[multifn qualifier dispatch-val f unique-key]
{:pre [(some? multifn)]}
Expand All @@ -179,8 +179,8 @@
;;;; #### Low-level destructive operations

(defn alter-var-root+
"Like `alter-var-root`, but handles vars that are aliases of other vars, e.g. ones that have been imported via
Potemkin `import-vars`."
"Like [[clojure.core/alter-var-root]], but handles vars that are aliases of other vars, e.g. ones that have been
imported via Potemkin [[potemkin/import-vars]]."
[multifn-var f & args]
(let [{var-ns :ns, var-name :name} (meta multifn-var)
varr (if (and var-ns var-name)
Expand All @@ -189,32 +189,32 @@
(apply alter-var-root varr f args)))

(defn add-primary-method!
"Destructive version of `add-primary-method`. Operates on a var defining a Methodical multifn."
"Destructive version of [[add-primary-method]]. Operates on a var defining a Methodical multifn."
[multifn-var dispatch-val f]
(alter-var-root+ multifn-var i/add-primary-method dispatch-val f))

(defn remove-primary-method!
"Destructive version of `remove-primary-method`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-primary-method]]. Operates on a var defining a Methodical multifn."
[multifn-var dispatch-val]
(alter-var-root+ multifn-var i/remove-primary-method dispatch-val))

(defn remove-all-primary-methods!
"Destructive version of `remove-all-primary-methods`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-all-primary-methods]]. Operates on a var defining a Methodical multifn."
[multifn-var]
(alter-var-root+ multifn-var remove-all-primary-methods))

(defn add-aux-method!
"Destructive version of `add-aux-method`. Operates on a var defining a Methodical multifn."
"Destructive version of [[add-aux-method]]. Operates on a var defining a Methodical multifn."
[multifn-var qualifier dispatch-val f]
(alter-var-root+ multifn-var i/add-aux-method qualifier dispatch-val f))

(defn remove-aux-method!
"Destructive version of `remove-aux-method`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-aux-method]]. Operates on a var defining a Methodical multifn."
[multifn-var qualifier dispatch-val f]
(alter-var-root+ multifn-var i/remove-aux-method qualifier dispatch-val f))

(defn remove-all-aux-methods!
"Destructive version of `remove-all-aux-methods`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-all-aux-methods]]. Operates on a var defining a Methodical multifn."
([multifn-var]
(alter-var-root+ multifn-var remove-all-aux-methods))

Expand All @@ -225,30 +225,31 @@
(alter-var-root+ multifn-var remove-all-aux-methods qualifier dispatch-val)))

(defn remove-all-aux-methods-for-dispatch-val!
"Destructive version of `remove-all-aux-methods-for-dispatch-val`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-all-aux-methods-for-dispatch-val]]. Operates on a var defining a Methodical multifn."
[multifn-var dispatch-value]
(alter-var-root+ multifn-var remove-all-aux-methods-for-dispatch-val dispatch-value))

(defn add-aux-method-with-unique-key!
"Destructive version of `add-aux-method-with-unique-key`. Operates on a var defining a Methodical multifn."
"Destructive version of [[add-aux-method-with-unique-key]]. Operates on a var defining a Methodical multifn."
[multifn-var qualifier dispatch-val f unique-key]
(alter-var-root+ multifn-var add-aux-method-with-unique-key qualifier dispatch-val f unique-key))

(defn remove-aux-method-with-unique-key!
"Destructive version of `remove-aux-method-with-unique-key`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-aux-method-with-unique-key]]. Operates on a var defining a Methodical multifn."
[multifn-var qualifier dispatch-val unique-key]
(alter-var-root+ multifn-var remove-aux-method-with-unique-key qualifier dispatch-val unique-key))

(defn remove-all-methods!
"Destructive version of `remove-all-methods`. Operates on a var defining a Methodical multifn."
"Destructive version of [[remove-all-methods]]. Operates on a var defining a Methodical multifn."
[multifn-var]
(alter-var-root+ multifn-var remove-all-methods))

(defn prefer-method!
"Destructive version of `prefer-method`. Operates on a var defining a Methodical multifn.
"Destructive version of [[prefer-method]]. Operates on a var defining a Methodical multifn.
Note that vanilla Clojure `prefer-method` is actually itself destructive, so this function is actually the
Methodical equivalent of that function. `prefer-method!` is used by Methodical to differentiate the operation from
our nondestructive `prefer-method`, which returns a copy of the multifn with an altered dispatch table."
Note that vanilla Clojure [[clojure.core/prefer-method]] is actually itself destructive, so this function is
actually the Methodical equivalent of that function. `prefer-method!` is used by Methodical to differentiate the
operation from our nondestructive [[prefer-method]], which returns a copy of the multifn with an altered dispatch
table."
[multifn-var dispatch-val-x dispatch-val-y]
(alter-var-root+ multifn-var i/prefer-method dispatch-val-x dispatch-val-y))
127 changes: 127 additions & 0 deletions src/methodical/util/trace.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
(ns methodical.util.trace
(:require [clojure.string :as str]
[methodical
[interface :as i]
[util :as u]]
[puget.printer :as puget]))

(def ^:dynamic *color*
"Whether or not to print the trace in color. True by default, unless the env var `NO_COLOR` is true."
(if-let [env-var-value (System/getenv "NO_COLOR")]
(Boolean/parseBoolean env-var-value)
true))

(def ^:dynamic *pprinter*
"Pretty-printer function to use for pretty printing forms in the trace. You can bind this to override the default
pretty-printing functions (see below)."
nil)

(defn- default-color-printer [x]
;; don't print in black. I can't see it
(binding [puget/*options* (assoc-in puget/*options* [:color-scheme :nil] nil)]
(puget/cprint x)))

(def ^:private default-boring-printer puget/pprint)

(defn- pprint
"Pretty print a form `x`."
[x]
((or *pprinter*
(if *color*
default-color-printer
default-boring-printer)) x))

(def ^:private ^:dynamic *trace-level*
"Current depth of the trace."
0)

(def ^:private ^:dynamic *trace-indent*
"Number of spaces to indent lines when printing stuff."
0)

(defn- trace-print-indent []
(doseq [_ (range *trace-indent*)]
(print " ")))

(defn- trace-println [& args]
(let [[first-line & more] (str/split-lines (str/trim (with-out-str (apply println args))))]
(println first-line)
(doseq [line more]
(trace-print-indent)
(println line))))

(defn- describe-method [a-method]
(let [{:keys [qualifier dispatch-value]} (meta a-method)]
(symbol (if qualifier
(format "#aux-method<%s %s>" (pr-str qualifier) (pr-str dispatch-value))
(format "#primary-method<%s>" (pr-str dispatch-value))))))

(defn- describe [x]
(cond
(::description (meta x)) (::description (meta x))
(not (fn? x)) x
(:dispatch-value (meta x)) (describe-method x)
(:methodical/combined-method? (meta x)) (symbol "#combined-method")))

(defn- trace-method [m]
(fn [& args]
(trace-print-indent)
(printf (format "%d: " *trace-level*))
(binding [*trace-indent* (+ *trace-indent* 3)]
(trace-println (with-out-str (pprint (map describe (cons m args))))))
(let [result (binding [*trace-level* (inc *trace-level*)
*trace-indent* (+ *trace-indent* 2)]
(apply m args))]
(trace-print-indent)
(printf "%d> " *trace-level*)
(binding [*trace-indent* (+ *trace-indent* 3)]
(trace-println (with-out-str (pprint result))))
result)))

(defn- trace-primary-method [primary-method]
(-> (trace-method primary-method)
(with-meta (meta primary-method))))

(defn- trace-primary-methods [primary-methods]
(map trace-primary-method primary-methods))

(defn- trace-aux-method [aux-method]
(-> (trace-method aux-method)
(with-meta (meta aux-method))))

(defn- trace-aux-methods [qualifier->ms]
(into {} (for [[qualifier aux-methods] qualifier->ms]
[qualifier (for [aux-method aux-methods]
(trace-aux-method (vary-meta aux-method assoc :qualifier qualifier)))])))

(defn trace*
"Function version of `trace` macro. The only difference is this doesn't capture the form of `multifn` passed to
`trace`, and thus can't usually generate a pretty description for the top-level form."
[multifn & args]
(let [dispatch-value (apply i/dispatch-value multifn args)
primary-methods (trace-primary-methods (u/matching-primary-methods multifn dispatch-value))
aux-methods (trace-aux-methods (u/matching-aux-methods multifn dispatch-value))
combined (-> (i/combine-methods multifn primary-methods aux-methods)
(with-meta (meta multifn))
trace-method)]
(apply combined args)))

(defmacro trace
"Instrument a multimethod `multifn`, then invoke it; calls to its primary and aux methods and their results are
printed to *out*`. Returns same result as untraced version would have returned. Prints trace in color by default,
but you can disable this by binding [[*color*]] to `false`.
Method calls are printed with `n:`, where `n` is the current depth of the trace; the result of each method call is
printed with a corresponding `n>`:
(trace/trace my-fn 1 {})
;; ->
0: (my-fn 1 {})
1: (#primary-method<:default> nil 1 {})
1> {:x 1}
1: (#aux-method<:after [java.lang.Object :default]> 1 {:x 1})
1> {:object? true, :x 1}
0> {:object? true, :x 1}"
[multifn & args]
`(trace* (vary-meta ~multifn assoc ::description '~multifn)
~@args))
Loading

0 comments on commit ea67b8a

Please sign in to comment.