Debugging and performance¶
A view looks stale, something feels slow, or you cannot tell which control owns a draft. Most of the answers are already values you can print — and you climb a performance ladder before you reach for the compiler.
Interpretation cost is rarely the problem. Fix subscriptions and boundaries first.
Questions and where to look¶
| Question | Where to look |
|---|---|
| What will this button dispatch? | The structural tree — (t/attrs node) carries the event vector |
| What is this view, exactly? | (v/describe view) — id, source, lowering, children policy, props schema |
| What can the compiler prove about it? | (v/manifest view) — nil for an interpreted declaration |
| Is this value even a view? | (v/view? x) — ifn? is not a proxy for it |
| Which host behaviors are connected? | (v/active-connections) |
| Did that command reach its target? | (v/command-log) |
| What dispatched this event? | the ordinary re-frame trace stream, and Xray over it |
| Why did this view re-render? | Xray's reactive panel; then narrow the subscription |
| Should I compile? | only after the ladder below |
Inspecting a declaration¶
v/describe is the descriptor's public projection — a plain map, and inspection
data rather than a dispatch surface:
(v/describe todo-row)
;; => {:re-frame.freehand/view true
;; :view-id :app.todo/todo-row
;; :source {:ns … :file … :line …}
;; :lowering :interpreted ; or :compiled
;; :children-policy :optional
;; :props-schema <schema>} ; absent when none declared
:props-schema is absent when no schema was declared, never :any — an
undeclared contract and a deliberately permissive one are different facts, and the
projection refuses to blur them.
v/manifest answers what a compiled declaration's analysis makes statically
knowable, and nil for an interpreted one — which is the honest answer, not an
omission:
(v/manifest people-list)
;; => {:view-id :app.people/people-list
;; :grammar :re-frame.freehand/v1
;; :crossings [{:view-id :re-frame.freehand/markup
;; :lowering :interpreted :path [1]}]}
:crossings is the roster of view boundaries the body mounts, one entry per
lexical site, each marked with the mode it crosses into. A compiled view mounting
an interpreted child is the ordinary case — promotion is per declaration and never
transitive — so the manifest says where the compiled tier stops rather than
leaving you to assume it does not.
The behavior tool plane¶
A registered behavior is the one place in the substrate where opaque host state lives, so it is the one place you most need to see into. Two read-only projections sit on the public door, so a tool never has to depend on an implementation namespace.
(v/active-connections)
;; => [{:generation 1
;; :behavior :my.ns/autosize
;; :frame :rf/default
;; :target :composer/body
;; :config {:max-rows 8}}]
Every live connection, oldest first: which behaviors are connected, under which
frame, claiming which semantic ids, on which public config. :generation and
:behavior are always present; :frame, :target and :config appear only where
the connection has one — so a behavior nothing commands projects no :target,
absent rather than nil.
The private memory and the host node are absent by construction, not filtered out. A projection that answered with a live host instance would be exactly the instance registry the behavior contract exists to refuse, reached through the inspection door instead of the front one.
(v/command-log)
;; => [{:frame :rf/default
;; :target :composer/body
;; :op :refit
;; :behavior :my.ns/autosize
;; :generation 1
;; :outcome :delivered}]
Recent command traffic as a bounded window, oldest first. A row records what
the command named and what the channel decided — :outcome is :delivered or
:refused — never what the host made of it, because a command's return value is
ignored: it is no handle, and it does not replace the connection's private
memory either. A delivered row is written before the operation runs, so a
command that crashes its host is in the log rather than missing from it.
Refusals are recorded as faithfully as deliveries. A log that only saw the successes would be evidence for the one case nobody debugs.
Both are browser-only, absent on the JVM exactly like the mount verbs: a
structural render connects nothing, and an eternal [] there would be
present-and-lying where absence is honest. Neither is an event stream — you ask,
and nothing calls you back.
The obvious summaries are deliberately not published, because you can compute
them: (count (v/active-connections)) and
(into #{} (keep :target) (v/active-connections)) are the same answers.
There is no occurrence accumulator¶
Freehand publishes no per-render history: no list of which occurrences rendered recently, why, or how often. The two projections above are the whole tool plane, and they cover host connections rather than view renders.
That is a stated omission rather than an oversight. For per-render questions — which occurrence re-rendered, and what caused it — use Xray, which reads the development-only evidence surface directly.
Performance ladder (before compilation)¶
Work in this order:
- Find the cause. Which query moved, or did the parent simply re-render?
- Narrow the subscription. The classic bug: every cell reads one global “editing id”, so a single click dirties two thousand cells instead of two. Fix it with per-id queries.
- Choose the boundary. Keyed child views for rows that change independently;
plain
defnhelpers for pure structure you are happy to re-run with the parent. - Window large lists. Do not mount what the user cannot see — see Reactivity.
- Measure again.
- Only then promote a remaining hot boundary with
{:compiled true}, or a library leaf that needs static proof.
Subscription results are stabilised with rf=, and each boundary is memoised on
its one props map, so churn with identical output almost always means over-broad
subscriptions or props rebuilt on every parent render — not interpretation cost.
If a host library or React reconciliation dominates the budget, another Hiccup
compiler will not save you: fix the host boundary or the data size.
Traces¶
Freehand dispatches ordinary re-frame events, so the ordinary trace stream is the record of what happened. Nothing about a Freehand view invents a second channel: an event fired from a view is the same event fired from anywhere else, with the same epoch, the same interceptors and the same observability sink.
Two things worth knowing while reading a trace of a typing session:
- Each keystroke on a controlled field is its own event. Typing
hellois about five dispatches. The stream looking busy is expected, not a defect — see Events. - A render-failure summary is bounded on purpose.
v/error-boundary's:on-errorcarries a stable diagnostic id, the failing view id, phase, fingerprint and evidence — never the exception, props, app-db or event payloads. The exception itself rides a separate private channel to the always-on error axis.
Xray¶
Xray is the panel over all of this: the trace stream, time-travel scrubbing, the derivation graph, and — in development builds — the per-occurrence view evidence the door does not publish.
That evidence is development-only and stripped from production bundles, which is the trade: a production build carries no render history, and a development build can tell you which occurrence rendered, under which frame and descriptor generation, and which reads and event sites were selected in it.
Evidence always states how complete it is. Freehand does not claim omniscience over a full-Clojure interpreted body: a compiled view's read sites are proven statically, an interpreted view's are the reads a committed render actually made, and a host interior is opaque. Those are labels on one grid, not separate modes.
If something feels wrong¶
| Symptom | Fix |
|---|---|
(v/manifest view) is nil |
the declaration is interpreted — that is the honest answer, not a failure |
(v/active-connections) is empty in a JVM test |
it is browser-only; a structural render connects nothing |
| A command did nothing | check (v/command-log) for a :refused row and the :target it named |
| Whole list re-renders on one edit | per-id subscriptions and keyed row boundaries |
| High churn, identical output | props rebuilt inline every parent render, or an over-broad subscription |
| Reaching for a render-history verb | there is none on the door — use Xray |