Diagnostics¶
Use Xray when a view re-renders unexpectedly or an interaction feels slow and you need to find the cause.
Xray is development tooling. It loads beside the application, reads the trace the runtime already emits, and requires no instrumentation in your views. It is removed from production builds.
Diagnose an interaction¶
- Load Xray through the development preload (Load Xray).
- Reproduce the click, keystroke, or update.
- Open Xray's Fresco tab and pick the view that matches your question: Mounted, Reads, Intents, Why, Advisor, or Causal.
- Read the cause, fan-out, and cost it reports.
Everything below happens in the Fresco tab, in Dynamic mode. The Causal view follows whichever dispatch is selected in Xray's event list. The Fresco tab documents each view and how to read an empty one. This chapter covers the application side: what the cause is and what to change.
An epoch is one event pipeline run, from dispatch through its state commit. Xray organises its evidence around epochs.
Load Xray¶
Put the dependency in a dev alias and the preload in the dev build, never in a
release build. While re-frame2 is pre-alpha the dependency is a checkout-local
one, relative to your own deps.edn.
;; deps.edn
{:aliases
{:dev
{:extra-deps
{day8/re-frame2-xray {:local/root "../re-frame2/tools/xray"}}}}}
The preload namespace is day8.re-frame2-xray.preload:
Xray renders into a host element your page reserves, marked
data-rf-xray-host:
That is the whole setup. The preload opens Xray into the host once the adapter
is ready, so you do not call init! yourself. Ctrl+Shift+C hides and shows
the panel. Xray's installation chapter covers
styling the host, a different selector, jump-to-source, and popping out to a
second window.
Why did this view run?¶
The Fresco tab's Why view answers this for one view at a time. It separates what it can prove from what it can only suggest:
| What the row says | Meaning | Typical response |
|---|---|---|
| Its own reads moved | :latest-reads: the view's subscriptions whose values changed most recently |
Check whether the read belongs lower in the tree, or whether the subscription is too coarse |
| Leads, not a cause | :candidates: recent dispatches that recomputed a subscription this view reads |
Places to look; Fresco records no link from a commit to the dispatch that caused it |
| Nothing was searched | :cap: no dispatch is retained in the frames this view reads, or the view reads nothing |
Reproduce the interaction and check again. Retention matters only if it was set to 0; (rf/configure! {:trace-buffer {:events-retained 50}}) restores the default |
Props, context, a parent host, a retried or discarded render, a bail-out, the commit and the paint are not causes Fresco records. Use the React DevTools Profiler for the run and commit, and the browser's performance tools for the paint.
The causal chain¶
Do not collapse render, commit, and paint into one number:
- a body can run speculatively;
- React decides what commits;
- the browser decides when it paints.
Xray labels which stage each measurement belongs to. Timing proximity alone does not prove causation.
The Causal view walks one dispatch along this chain:
event
→ subscriptions recomputed
→ values changed
→ views notified
→ bodies run
→ React commit
→ browser paint
Xray correlates links only where an instrument can support the relationship. When it cannot, it reports that limitation instead of guessing.
Read topology and fan-out¶
The Fresco tab's Reads view maps subscriptions to the currently committed views that read them. Four measurements usually identify the shape:
| Measurement | What it reveals |
|---|---|
| View count | Number of independently re-rendering units currently mounted |
| Reads per view | Fine-grained, coarse, or accidentally enormous read sets |
| Fan-out per subscription | Number of views one changed value can notify |
| Read-set churn | How often a view changes which subscriptions it reads |
When several views run for one event, fan-out distinguishes a topology problem from independent useful work. One changed subscription reaching hundreds of readers deserves attention. Hundreds of unrelated reads changing together may be the intended update.
Two common failures:
- One subscription fans out to hundreds of views because a shared read lives too high or the read model is too broad.
- A view changes its read membership every render and pays to replace the complete committed read set each time.
Move reads, change view boundaries, or choose fine, coarse, chunked, or windowed collection reads as described in Views and reads and Lists and collections.
Use attribution before choosing a fix¶
The Fresco tab's Advisor view ranks views by time, frequency, read churn, and fan-out. It first identifies where the time is going:
| Pressure | Cost owner | Smallest credible fix |
|---|---|---|
| Computation | View code or an expensive subscription chain | Move or reduce the computation; derive display values in subscriptions |
| Topology | Too many invalidated views or unstable read sets | Move reads, split or combine views, or change collection read shape |
| Hiccup conversion | Turning one hot view's Hiccup into React elements | Return a React element directly from that same view (Islands) |
| React | Reconciliation, hooks, or vendor internals | Use a React island, raw or UIx (Islands) |
| Layout and paint | Browser style, layout, and rendering | Reduce DOM, virtualise, or fix CSS; use browser tooling |
The advisor measures only the first two rows: computation from subscription timings, and topology from fan-out, read order and recompute counts. It names the other three but does not rank them. Chrome's timer has a 0.1 ms grain while a view body costs a few microseconds, so per-view time would be noise, and commit and paint belong to React.
So the advisor never recommends a native escape, even for the hottest view on
the page: its evidence cannot show that Hiccup conversion or React owns the
cost. Check the last three rows yourself, each with its own tool: the
rf:render:<view-id> User Timing measures for Hiccup conversion (off by
default; see Performance), the React
DevTools Profiler for React, and browser performance tools for layout and
paint. Any native escape must still pass the thresholds in
Performance.
Incomplete evidence is reported explicitly¶
Xray uses named completeness states instead of pretending that missing evidence is an empty result:
| Label | Meaning | Response |
|---|---|---|
:unknown |
No instrument covers the requested relationship | Ask a question the instruments can answer, or encode the claim in a test |
:opaque |
The runtime does not record this fact, by design | Ask a question the instruments do hold |
:host-opaque |
React owns this fact and does not publish it (commit and paint for every view) | Use React DevTools and the browser performance tools |
:cap |
The bounded history has dropped older evidence | Reproduce and capture a fresh epoch |
:uncorrelated |
The fact is real, but no id links it to a cause | A rerun does not change it; read the leads offered and confirm the link yourself |
Complaint IDs¶
Fresco errors and warnings use stable identifiers:
:rf.error/*for errors;:rf.warning/*for recoverable misuse.
A thrown error places the id in ex-data under :rf.error/id. Fresco's own
errors are throws, and its warnings are development console lines, printed once
per site with the id in the message. Core ids such as :rf.error/frame-destroyed
also appear as trace records, carrying the recovery the runtime applied. Xray can link to
the registration site and the call site when source data is available.
Troubleshooting lists every Fresco id with its cause and fix.
Follow the named recovery before changing unrelated code.
To test a refusal, assert its id; see Testing.
Verify production erasure¶
A release build removes Xray, Fresco's evidence collection, development
warnings and source locations. An error that can still fire in production keeps
its id and message. Evidence queries return nil in production. Performance instrumentation has a separate compile-time flag and is
off by default.
Verify erasure with a positive control:
npx shadow-cljs compile app
grep -c "rf.xray" public/js/main.js # expect more than 0
npx shadow-cljs release app
grep -c "rf.xray" public/js/main.js # expect 0
The development build must show a count above 0. Zero in both builds means the search is broken, not that erasure worked (the sabotage-twin idea from Testing).
Application behaviour must never depend on diagnostics. Do not branch on whether evidence exists, count warnings as product data, or read panel state from application code.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| A view you expected is absent from the epoch | Its props and reads allowed it to skip; a body that did not run emits no occurrence | Treat absence as work avoided. Inspect the parent occurrence when you expected different props |
Explain-render returns :uncorrelated |
Fresco records no link from a commit to the dispatch that caused it | Expected; a bigger history does not change it. Work from the :candidates leads |
Every view reports :host-opaque after its body ran |
React owns commit and paint for all views | Expected. Use React DevTools for the run and the commit, browser tools for the paint |
History ends with :cap |
The bounded retention window discarded old epochs | Reproduce the issue and capture it again |
| The advisor will not recommend a native island | It cannot measure the costs native code fixes | Apply the remedy it names, or measure with the tool it points to |
| Repeated runs have different timings | Xray timing is diagnostic attribution, not a controlled benchmark | Use the cost classification; benchmark under Performance |
| A complaint id has no catalogue entry | The id belongs to another namespace, or application and test-kit versions differ | Check the namespace and align installed versions |
| Panels are empty in a release build | Diagnostics were erased as designed | Diagnose with a development build |
When Xray is not the right tool¶
Xray identifies the cost owner and the likely class of remedy. It does not answer whether an interaction meets a production budget. Use the measurement method in Performance for that.
Use React DevTools for commit-level React details and browser performance tools for layout and paint. Profile release builds for production slowness; a development build intentionally contains development work.
When the question is correctness rather than cause, write a test (Testing).
Advanced¶
Explain-render envelope¶
The Why view, tests, and an AI pair all read one versioned evidence map,
produced by
re-frame.fresco.tool/explain-render.
Call it yourself only for scripted diagnosis; it returns nil in a release
build. A representative entry:
{:boundary {:parent nil :key [[:app :todo/by-id [:todo/by-id 7]]]}
:views [{:view "todo.views/todo-row"
:source {:ns todo.views :file "src/todo/views.cljs" :line 41 :column 1}}]
:frame :app
:instances 1
:window {:frames [:app] :retained-runs 12}
:snapshot 9
:peak-epoch 5
:latest-reads [{:sub-id :todo/by-id :query [:todo/by-id 7] :frame-id :app}]
:loss {:reason :uncorrelated :dropped :unknown}
:candidates [{:dispatch-id 41 :event-id :todo/toggle :frame-id :app :sub-id :todo/by-id}]}
:latest-reads is proven: the reads whose values changed most recently.
:candidates are leads, not a cause, because Fresco records no link from a
commit to its dispatch; :loss says so. The enclosing map also names its
schema version and states its own completeness.
Privacy projection¶
Xray and the AI pair consume the same privacy-projected evidence schema. Query arguments pass through the projector. Raw values and text are omitted by default. Data leaves the process only through an authorised consumer.
Optional-module evidence¶
The resources module adds its own tooling view: which owners hold an entry, and the cause that started each fetch. The forms, overlay and motion modules add no evidence of their own.