Skip to content

Commit

Permalink
Add tufte guide
Browse files Browse the repository at this point in the history
  • Loading branch information
djblue committed Jul 26, 2023
1 parent faf88c1 commit 223d49f
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 0 deletions.
1 change: 1 addition & 0 deletions doc/cljdoc.edn
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
["Portal Console" {:file "doc/guides/portal-console.md"}]
["Timbre" {:file "examples/timbre/README.md"}]
["μ/log" {:file "examples/mulog/README.md"}]]
["Tufte Profiling" {:file "examples/timbre/README.md"}]
["JavaScript Promises" {:file "doc/guides/promises.md"}]
["Custom Tap List" {:file "doc/guides/custom-taps.md"}]
["Default Viewer" {:file "doc/guides/default-viewer.md"}]
Expand Down
99 changes: 99 additions & 0 deletions examples/tufte/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Tufte Setup Guide

If you are a [ptaoussanis/tufte](https://github.com/ptaoussanis/tufte) user,
this guide will help you get profiling data into Portal.

The main advantages of using Portal are that your profiling data is always
available as data and the :loc data can be used to jump directly to the source
location using the `goto-definition` [command][commands].

### Setup

To get started, you need the following namespaces:

``` clojure
(ns user
(:require [portal.api :as p]
[taoensso.tufte :as tufte :refer (p profiled profile)]))
```

Next, you need to map tufte pstats data to something that can be used by the
Portal table viewer. Below is one such mapping:

```clojure
(def columns
(-> [:min :p25 :p50 :p75 :p90 :p95 :p99 :max :mean :mad :sum]
(zipmap (repeat :portal.viewer/duration-ns))
(assoc :loc :portal.viewer/source-location)))

(defn format-data [stats]
(-> stats
(update-in [:loc :ns] symbol)
(vary-meta update :portal.viewer/for merge columns)))

(defn format-pstats [pstats]
(-> @pstats
(:stats)
(update-vals format-data)
(with-meta
{:portal.viewer/default :portal.viewer/table
:portal.viewer/table
{:columns [:n :min #_:p25 #_:p50 #_:p75 #_:p90 #_:p95 #_:p99 :max :mean #_:mad :sum :loc]}})))
```

With the above mapping, you can plug into tufte via its handler mechanism:

```clojure
(defn add-tap-handler!
"Adds a simple handler that logs `profile` stats output with `tap>`."
[{:keys [ns-pattern handler-id]
:or {ns-pattern "*"
handler-id :basic-tap}}]
(tufte/add-handler!
handler-id ns-pattern
(fn [{:keys [?id ?data pstats]}]
(tap> (vary-meta
(format-pstats pstats)
merge
(cond-> {}
?id (assoc :id ?id)
?data (assoc :data ?data)))))))
```

### 10-second example

Borrowing from the [10 second example in the tufte docs][tufte-example], we have
the equivalent:

```clojure
;; Open the Portal UI to see the output:
(require '[portal.api :refer (open submit)])
(add-tap submit)
(open)

(require '[taoensso.tufte :as tufte :refer (defnp p profiled profile)])

;; We'll request to send `profile` stats to `tap>`:
(add-tap-handler! {})

;;; Let's define a couple dummy fns to simulate doing some expensive work
(defn get-x [] (Thread/sleep 500) "x val")
(defn get-y [] (Thread/sleep (rand-int 1000)) "y val")

;; How do these fns perform? Let's check:

(profile ; Profile any `p` forms called during body execution
{} ; Profiling options; we'll use the defaults for now
(dotimes [_ 5]
(p :get-x (get-x))
(p :get-y (get-y))))
```

Which will produce the following output:

> **Note** goto-definition will only work if `taoensso.tufte/p` calls are eval'd
> via a repl that properly provide file/line/column info. This includes using
> nrepl or using load-file to load your code.
[tufte-example]: https://github.com/ptaoussanis/tufte#10-second-example
[commands]: ../../doc/ui/commands.md
3 changes: 3 additions & 0 deletions examples/tufte/deps.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{:deps
{djblue/portal {:local/root "../../"}
com.taoensso/tufte {:mvn/version "2.5.1"}}}
60 changes: 60 additions & 0 deletions examples/tufte/src/user.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
(ns user
"Fork of https://github.com/ptaoussanis/tufte#10-second-example for tap> and Portal"
(:require [portal.api :as p]
[taoensso.tufte :as tufte :refer (p profiled profile)]))

(def columns
(-> [:min :p25 :p50 :p75 :p90 :p95 :p99 :max :mean :mad :sum]
(zipmap (repeat :portal.viewer/duration-ns))
(assoc :loc :portal.viewer/source-location)))

(defn format-data [stats]
(-> stats
(update-in [:loc :ns] symbol)
(vary-meta update :portal.viewer/for merge columns)))

(defn format-pstats [pstats]
(-> @pstats
(:stats)
(update-vals format-data)
(with-meta
{:portal.viewer/default :portal.viewer/table
:portal.viewer/table
{:columns [:n :min #_:p25 #_:p50 #_:p75 #_:p90 #_:p95 #_:p99 :max :mean #_:mad :sum :loc]}})))

(defn add-tap-handler!
"Adds a simple handler that logs `profile` stats output with `tap>`."
[{:keys [ns-pattern handler-id]
:or {ns-pattern "*"
handler-id :basic-tap}}]
(tufte/add-handler!
handler-id ns-pattern
(fn [{:keys [?id ?data pstats]}]
(tap> (vary-meta
(format-pstats pstats)
merge
(cond-> {}
?id (assoc :id ?id)
?data (assoc :data ?data)))))))

;;; Let's define a couple dummy fns to simulate doing some expensive work
(defn get-x [] (Thread/sleep 500) "x val")
(defn get-y [] (Thread/sleep (rand-int 1000)) "y val")

(defn do-work []
(dotimes [_ 5]
(p :get-x (get-x))
(p :get-y (get-y))))

(defn run []
;; CLI usage
(println "Running profile...")
(-> (profiled {} (do-work)) second format-pstats p/inspect))

(comment
;; REPL usage
(p/open)
(add-tap p/submit)
(add-tap-handler! {})
(profile {} (do-work))
(remove-tap p/submit))

0 comments on commit 223d49f

Please sign in to comment.