A tiny, babashka-compatible profiler — the core of Tufte (p profile points, a profile
scope, and a println report) reimplemented with only plain atoms / volatiles / dynamic vars so
it runs under SCI. (Real Tufte does NOT load under babashka: its encore dependency uses a
defrecord implementing clojure.lang.Counted, which SCI rejects.)
Design goals:
p and profile are compile-time
gated on the fulcro.tui.perf system property: unless it is set when the calling code is
macro-expanded, both expand to just (do body...) — no runtime check, no volatile read, no
trace of profiling in the compiled code. So these points are safe to leave in shipped render
code, and the shipped library (compiled without the property) carries no profiling at all.src/main (the shipped render code calls p directly), but
pulls in nothing.p records both total (inclusive) and self (exclusive of
nested ps) time, so it is safe to instrument recursive code (e.g. the paint walker) and read
a meaningful per-id breakdown.Usage — first build the instrumentation IN by setting the property (JVM: -Dfulcro.tui.perf=1;
babashka: bb -Dfulcro.tui.perf=1 ...), then while running the TUI:
(perf/start!) ; enable + reset ;; ... exercise the code / interact with the app ... (perf/report) ; print the per-id table (self-time ranked) (perf/stop!) ; disable
Or scope it: (perf/profile {} (render! app)) enables for the dynamic extent, prints a report,
and returns the body value. Without the property set, start!/stop!/report still work but
have nothing to measure, since no p points were compiled in.
A tiny, babashka-compatible profiler — the core of Tufte (`p` profile points, a `profile`
scope, and a println report) reimplemented with only plain atoms / volatiles / dynamic vars so
it runs under SCI. (Real Tufte does NOT load under babashka: its `encore` dependency uses a
`defrecord` implementing `clojure.lang.Counted`, which SCI rejects.)
Design goals:
* **Truly zero overhead unless explicitly built in.** `p` and `profile` are *compile-time*
gated on the `fulcro.tui.perf` system property: unless it is set when the calling code is
macro-expanded, both expand to just `(do body...)` — no runtime check, no volatile read, no
trace of profiling in the compiled code. So these points are safe to leave in shipped render
code, and the shipped library (compiled without the property) carries no profiling at all.
* **No new dependencies.** Lives in `src/main` (the shipped render code calls `p` directly), but
pulls in nothing.
* **Self-time accounting.** Each `p` records both *total* (inclusive) and *self* (exclusive of
nested `p`s) time, so it is safe to instrument recursive code (e.g. the paint walker) and read
a meaningful per-id breakdown.
Usage — first build the instrumentation IN by setting the property (JVM: `-Dfulcro.tui.perf=1`;
babashka: `bb -Dfulcro.tui.perf=1 ...`), then while running the TUI:
(perf/start!) ; enable + reset
;; ... exercise the code / interact with the app ...
(perf/report) ; print the per-id table (self-time ranked)
(perf/stop!) ; disable
Or scope it: `(perf/profile {} (render! app))` enables for the dynamic extent, prints a report,
and returns the body value. Without the property set, `start!`/`stop!`/`report` still work but
have nothing to measure, since no `p` points were compiled in.When inside a p, a volatile! accumulating the total (inclusive) ns of this point's DIRECT
children, so the enclosing point can subtract it to get self time. nil at the top level.
When inside a `p`, a `volatile!` accumulating the total (inclusive) ns of this point's DIRECT children, so the enclosing point can subtract it to get self time. `nil` at the top level.
(cache f)(cache {:keys [ttl-ms gc-every] :or {gc-every 1000}} f)Returns a cached (memoized) version of referentially-transparent f.
Called with just f, entries are cached forever. Called with an options map, supports
time-based expiry. The options are:
:ttl-ms - Expire each entry this many milliseconds after it was computed. Omitted or
0 means no expiry.:gc-every - Sweep expired entries roughly once per this many calls (default 1000).
Only relevant when :ttl-ms is set.Like clojure.core/memoize but de-raced: concurrent first-calls for the same args run f
exactly once (the loser's delay is discarded unforced), so it is safe under contention.
The returned fn also reads a command keyword as its FIRST argument:
:cache/del - Drop the entry for the remaining args; returns nil. Pass
:cache/all as the next arg — (cached :cache/del :cache/all) — to clear everything.:cache/fresh - Force a recompute (and re-store) for the remaining args; returns the
new value.Mirrors the call shape of taoensso.encore/cache (:mem/del/:mem/fresh/:mem/all are
accepted as aliases) but is intentionally a small subset: no LRU/LFU size eviction.
Returns a cached (memoized) version of referentially-transparent `f`.
Called with just `f`, entries are cached forever. Called with an options map, supports
time-based expiry. The options are:
* `:ttl-ms` - Expire each entry this many milliseconds after it was computed. Omitted or
0 means no expiry.
* `:gc-every` - Sweep expired entries roughly once per this many calls (default 1000).
Only relevant when `:ttl-ms` is set.
Like `clojure.core/memoize` but de-raced: concurrent first-calls for the same args run `f`
exactly once (the loser's `delay` is discarded unforced), so it is safe under contention.
The returned fn also reads a command keyword as its FIRST argument:
* `:cache/del` - Drop the entry for the remaining args; returns nil. Pass
`:cache/all` as the next arg — `(cached :cache/del :cache/all)` — to clear everything.
* `:cache/fresh` - Force a recompute (and re-store) for the remaining args; returns the
new value.
Mirrors the call shape of `taoensso.encore/cache` (`:mem/del`/`:mem/fresh`/`:mem/all` are
accepted as aliases) but is intentionally a small subset: no LRU/LFU size eviction.(enabled?)Returns true when profiling is currently capturing.
Returns true when profiling is currently capturing.
(memoize f)(memoize ttl-ms f)Encore-familiar wrapper over cache. (memoize f) caches forever; (memoize ttl-ms f)
expires entries ttl-ms milliseconds after they are computed.
Encore-familiar wrapper over `cache`. `(memoize f)` caches forever; `(memoize ttl-ms f)` expires entries `ttl-ms` milliseconds after they are computed.
(p id & body)Profile point: times body under id (any value — keyword or syntax-quoted symbol) and returns
its value.
Gated at compile time on the fulcro.tui.perf system property (see instrument?): when the
property is unset this expands to just (do body...) — zero overhead, nothing compiled in. When
it is set, this compiles in instrumentation that is still gated at RUNTIME by enabled?, so
captured timing only accrues between start! and stop!. Records total (inclusive) and self
(exclusive of nested ps) time.
Profile point: times `body` under `id` (any value — keyword or syntax-quoted symbol) and returns its value. Gated at compile time on the `fulcro.tui.perf` system property (see `instrument?`): when the property is unset this expands to just `(do body...)` — zero overhead, nothing compiled in. When it is set, this compiles in instrumentation that is still gated at RUNTIME by `enabled?`, so captured timing only accrues between `start!` and `stop!`. Records total (inclusive) and self (exclusive of nested `p`s) time.
(profile _opts & body)Brackets body with start!/stop!, prints a report, and returns the body's value.
opts is accepted for Tufte-API familiarity but currently ignored.
Like p, gated at compile time on the fulcro.tui.perf system property: when it is unset this
expands to just (do body...) (no start/stop/report), so it is a true no-op in a build without
the property.
Brackets `body` with `start!`/`stop!`, prints a `report`, and returns the body's value. `opts` is accepted for Tufte-API familiarity but currently ignored. Like `p`, gated at compile time on the `fulcro.tui.perf` system property: when it is unset this expands to just `(do body...)` (no start/stop/report), so it is a true no-op in a build without the property.
(record! id total-ns self-ns)Folds one observation for id (total-ns inclusive, self-ns exclusive) into stats.
Public because the p macro expands to a call here, but normally only called via p.
Folds one observation for `id` (`total-ns` inclusive, `self-ns` exclusive) into `stats`. Public because the `p` macro expands to a call here, but normally only called via `p`.
(report)Prints report-string to stdout and returns nil.
Prints `report-string` to stdout and returns nil.
(report-string)Returns the formatted profiling report as a string (rows sorted by self time, descending). Percentages are share of total SELF time across all ids — i.e. where CPU time actually went.
Returns the formatted profiling report as a string (rows sorted by self time, descending). Percentages are share of total SELF time across all ids — i.e. where CPU time actually went.
(reset!)Clears all accumulated stats and restarts the wall-clock bracket.
Clears all accumulated stats and restarts the wall-clock bracket.
(snapshot)Returns the current raw stats map (id -> aggregate), without printing.
Returns the current raw stats map (id -> aggregate), without printing.
(start!)Enables profiling and clears prior stats. Call before exercising the code to measure.
Enables profiling and clears prior stats. Call before exercising the code to measure.
(stop!)Disables profiling and freezes the wall-clock elapsed.
Disables profiling and freezes the wall-clock elapsed.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |