Skip to content

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.

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.

Live debug shell
This is the debug shell from Fleury terminal apps, rendered in the browser. Frame timings come from this browser session; paint flashing and terminal diagnosis require a native run.

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.

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.

Press Ctrl+G while your app runs from source, or launch the fuller playground from the packages/samples directory of a Fleury checkout:

Terminal window
cd packages/samples
dart run bin/samples.dart debug

Open 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.

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:

EvidenceWhat it tells you
SourcesWhich build, layout, or paint objects requested work in the latest frame.
Layouts: run / skippedWhether the frame reused layout results.
Boundaries: repainted / cachedWhether repaint boundaries reused painted content.
Dirty cells and spansHow 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.

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.

KeyAction
Ctrl+GOpen or close; preserve the recording.
F12Open Logs; press again there to close.
f or F11Expand or dock the open panel.
Tab / Shift+Tab, or left/rightSwitch tabs.
Page Up / Page DownScroll Live, Tree, Rebuilds, or Errors.
Up / Down, HomeSelect a node in Tree; Home returns to the first.
/ and sSearch Logs; switch between stdout, stderr, and both.
pTurn paint flash on or off.
EscClear a log filter, otherwise dock an expanded panel.
F5Hot 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.

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.

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:

  1. In a terminal, from the app’s package directory, run fleury shell. It prints fleury shell ready and waits.
  2. Run or debug the app from the IDE. The app finds the shell through the .fleury/handle file the shell wrote, draws in the shell’s terminal, and takes its input from it, while the IDE keeps its console and debugger.
  3. 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.

An agent can drive the same playground and read frame, log, and error records. From packages/samples:

Terminal window
fleury_mcp -- dart run bin/samples.dart debug

Use 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.