Debugging
The debug shell connects a rendered frame to its phase timings, invalidation sources, logs, and errors. Open it with Ctrl+G in a development run (when it’s on); it overlays the app without changing the app’s layout.
Reproduce a slow frame
Section titled “Reproduce a slow frame”Click Slow build below, then find Worst build in Rebuilds. The app blocks one build for about 100 ms; the panel measures the actual frame. Switch to Errors after clicking Throw error, or Logs after Write logs. Use Fullscreen demo for more room; Expand panel gives the debug shell the whole app surface.
Build dominates here. Cache repeated computation when its inputs change; for substantial CPU work, use a worker isolate in native apps. Awaiting I/O frees the UI, but making a CPU-heavy function async does not. If layout dominates, inspect layout work and cache misses. If paint or diff dominates, inspect the changed region and cell counts first.
Use Reset demo to start a fresh recording. In your own app, repeat the same interaction after changing the code. Check responsiveness as well as timings at the same terminal size; the >16 ms count is a useful flag, not a complete measure of input latency. A busy app can replace all 60 recorded frames quickly.
Follow an error to its caller
Section titled “Follow an error to its caller”Click Throw error in the demo, then open Errors. The callback fails, but the app keeps running. Find the save handler in the stack trace; in your own app, start with the first relevant application frame. The last 50 errors remain available after their banners disappear, newest first.
After fixing the cause, repeat the action and check for a new timestamp; old entries remain in history. For an expected load failure, render an error state and recovery action using Loading data.
To inspect the events leading to a failure, click Write logs and open
Logs. Press / and type sync: to isolate the 12 new lines. Enter
keeps the filter, Esc clears it, and s switches between stdout, stderr,
and both.
Use the debug shell locally
Section titled “Use the debug shell locally”Press Ctrl+G while your app runs from source, or launch the fuller
playground from the packages/samples directory of a Fleury checkout:
cd packages/samplesdart run bin/samples.dart debugOpen the debug shell once to start recording, then close it so the app receives its normal keys. Trigger Spike a slow frame, reopen the panel, and select Rebuilds. The recording keeps the last 60 frames while hidden. Inspect the worst frame: reopening the panel can produce a newer, faster one.
On macOS and Linux, a terminal session also captures ordinary prints and
descriptor-level output, including FFI writes, so stray output does not
overwrite the app. Captured output replays after exit. Windows doesn’t capture
output, and FLEURY_FD_CAPTURE=0 or an app that passes its own driver: to
runApp turns capture off. With fleury serve, check the server console.
Find unnecessary work
Section titled “Find unnecessary work”In the native playground, start Toggle live stream, then watch Sources in Rebuilds while updates continue. These are the latest frame’s invalidation origins, not a CPU stack trace. Look for state or notifier updates that reach a larger subtree than necessary. Close the panel and Stop live stream when done.
Opening the panel also schedules work, so the latest sources can belong to the debug shell. Worst sources retains the origins of the single frame with the highest total duration; it does not summarize the recording.
Press p while the panel is open to enable paint flash. Close the panel and repeat the interaction: the flash remains active, so you can compare the changed region with the update you intended. Reopen it and press p again to turn it off.
Use the counters to distinguish the work involved:
| Evidence | What it tells you |
|---|---|
| Sources | Which build, layout, or paint objects requested work in the latest frame. |
| Layouts: run / skipped | Whether the frame reused layout results. |
| Boundaries: repainted / cached | Whether repaint boundaries reused painted content. |
| Dirty cells and spans | How much changed in the final cell diff. A rebuild need not produce terminal output. |
A repaint boundary helps when painting is the cost you can isolate. It does not remove expensive build work. See Performance for the rendering model and State management for narrowing subscriptions.
Inspect an action that does not respond
Section titled “Inspect an action that does not respond”Open Tree and inspect the control’s semantic label, enabled state, focus, and exposed actions. This is the app’s semantic tree, not its widget or render tree. Use the up/down arrows to select a node and page through its details.
A missing or disabled semantic action points to the control or command wiring. If the action works through an agent but the physical key does not, inspect Live → Keyboard and the binding’s scope. See Key handling and Focus management for those contracts. The terminal profile in Tree also helps separate app behavior from a terminal fallback.
Keep the debug shell out of the reproduction
Section titled “Keep the debug shell out of the reproduction”The app keeps running beneath the panel, but Fleury gives the debug shell its
keys before any of your app’s bindings see them. Ctrl+G and F12 belong
to the debug shell whenever it is on, even while the panel is closed, so an
app’s own Ctrl+G or F12 binding works only with the debug shell off (see
Configuring it). While the panel is open, it also takes the
other keys below; close it to reproduce normal keyboard behavior.
Key handling
lists the other keys Fleury handles itself, including Ctrl+C and Ctrl+Z.
| Key | Action |
|---|---|
| Ctrl+G | Open or close; preserve the recording. |
| F12 | Open Logs; press again there to close. |
| f or F11 | Expand or dock the open panel. |
| Tab / Shift+Tab, or left/right | Switch tabs. |
| Page Up / Page Down | Scroll Live, Tree, Rebuilds, or Errors. |
| Up / Down, Home | Select a node in Tree; Home returns to the first. |
| / and s | Search Logs; switch between stdout, stderr, and both. |
| p | Turn paint flash on or off. |
| Esc | Clear a log filter, otherwise dock an expanded panel. |
| F5 | Hot restart while the panel is open and a development supervisor is attached; application state is lost. |
F5 and F11 work only when the key reaches the app. When F5 doesn’t restart covers F5, including the editor debug sessions where the restart is the editor’s. F11 is often taken first: VS Code’s integrated terminal on Windows and Linux, Windows Terminal, and GNOME Terminal use it for full screen, and macOS uses it to show the desktop. Press f instead.
Configuring it
Section titled “Configuring it”The debug shell and its F12 logs are on when the app runs from a .dart
source file (fleury run, dart run bin/run_app.dart,
fleury serve --spawn dart run …, fleury_mcp -- dart run …) or with assertions enabled (--enable-asserts).
They’re off in AOT executables (dart compile exe) and in snapshots, such as
the ones dart pub global activate installs; browser bundles don’t include
them. A dart run without a file path, or dart run package:exe, also runs a
precompiled snapshot, so name the file (dart run bin/run_app.dart) or use
fleury run during development. Pass a DebugConfig to runApp to change that either way, or to open
the panel at launch and record from startup:
await runApp( const MyApp(), debug: const DebugConfig( startMode: DebugMode.docked, panelWidth: 44, ),);enabled: false turns off the shell, its hotkeys, and its recording;
enabled: true turns them on in a compiled build.
startMode: DebugMode.fullscreen opens the panel expanded. A bottom panel
uses side: DebugPanelSide.bottom and panelHeight. For restarting and
editor integration, see Hot reload.
Debug with breakpoints
Section titled “Debug with breakpoints”The debug shell shows what the framework did. To step through your own code, run the app under a Dart debugger.
In VS Code with the
Dart extension,
press F5. fleury create writes a .vscode/launch.json that runs
bin/run_app.dart under the debugger in the integrated terminal, where the app
draws as usual, and a .vscode/settings.json that reloads on save.
Breakpoints, stepping, and the debug console work as they do for any Dart
program. Ctrl+G still opens the debug shell, but it offers no restart
there; use the editor’s Restart. Hot reload
shows how to set this up in an existing project.
IntelliJ IDEA and Android Studio run the app in a console that isn’t a
terminal, where runApp stops with “runApp needs an interactive terminal”.
fleury shell gives the app a terminal to draw in:
- In a terminal, from the app’s package directory, run
fleury shell. It printsfleury shell readyand waits. - Run or debug the app from the IDE. The app finds the shell through the
.fleury/handlefile the shell wrote, draws in the shell’s terminal, and takes its input from it, while the IDE keeps its console and debugger. - Leave the shell running. When the app exits, or you stop or restart it from the IDE, the shell waits for the next run, which attaches to it.
While an app is attached, every key goes to the app, Ctrl+C and Ctrl+Z included: a focused text field undoes on Ctrl+Z, and a Ctrl+C the app doesn’t handle ends it, as in a terminal of its own. A Ctrl+Z the app doesn’t handle suspends nothing, though: the app runs in the IDE, not as a job of the shell’s terminal. With no app attached, Ctrl+C quits the shell, and keys typed then reach no app.
The shell’s terminal reports what the app’s TerminalMode asks for, as a
terminal of its own would: clicks, drags, and the wheel with mouse: true
(the counter fleury create writes uses it), hover with mouseMotion: true,
and pastes and focus changes unless the mode turns them off. The shell turns
them on when the app attaches and off when it detaches, however the run ends.
It reads the mode the app passed to runApp once, at attach, so a changed
mode takes effect on the next run.
The app finds the shell when its working directory is that package directory
or one below it. If the IDE runs the app from somewhere else, set the
FLEURY_HANDLE environment variable in the run configuration to the socket
path the shell prints. Under the IDE’s debugger, which turns on the Dart VM
service, saving a file also reloads the app. The shell and the app must speak
the same version of the shell’s protocol, so run the shell as
dart run fleury shell in the app’s package: it then comes from the app’s own
Fleury. The shell turns away an app that speaks another version and says why.
fleury shell runs on macOS and Linux.
Share a reproducible investigation
Section titled “Share a reproducible investigation”An agent can drive the same playground and read frame, log, and error records.
From packages/samples:
fleury_mcp -- dart run bin/samples.dart debugUse read_frames, read_logs, and read_errors after the triggering action;
see Driving with an agent for setup. For a
terminal rendering issue, attach a saved diagnosis too. If the cell diff looks
correct but terminal output does not, launch with
FLEURY_ANSI_CAPTURE=/path/to/capture.ansi to capture the emitted bytes.