A demo app organized by category. 26 demo screens trigger specific
detectors, and 14 capture-helper screens drive runtimeVerified capture
brackets.
# Profile mode (recommended, accurate timing)
cd example && flutter run --profile --no-dds
# Debug mode (per-widget rebuild and paint counts, timing less representative)
cd example && flutter run --no-dds--no-dds lets Sleuth connect to the VM service, which its memory, CPU,
event-loop and time-share detectors need.
| # | Screen | Detectors triggered | Category |
|---|---|---|---|
| 1 | High-Level setState | SetStateScope; Rebuild in debug builds at 10 or more taps a second | Build |
| 2 | Rebuild Hotspot (Dashboard) | Rebuild stats in the More menu (profile); Rebuild (debug) | Build |
| 3 | Non-Lazy ListView | ListView | Build |
| 4 | Shrink-wrapped Sections | ListView (two cards, one per list) | Build |
| 5 | CSV Import | HeavyCompute | Build |
| 6 | Live Waveform | Repaint | Paint |
| 7 | Always-Repaint CustomPainter | CustomPainter | Paint |
| 8 | Missing RepaintBoundary | RepaintBoundary | Paint |
| 9 | GPU Pressure | GpuPressure | GPU & Rendering |
| 10 | Shader Jank | ShaderJank | GPU & Rendering |
| 11 | FPS Stress Test (~20 FPS) | HeavyCompute, GpuPressure | GPU & Rendering |
| 12 | Cupertino Blur | GpuPressure (expensive_gpu_nodes; one panel per blur shape, and the detail names the Cupertino bar or dialog that draws a blur) |
GPU & Rendering |
| 13 | IntrinsicHeight Abuse | LayoutBottleneck | Layout |
| 14 | Uncached Images | ImageMemory | Memory |
| 15 | Memory Pressure | MemoryPressure | Memory |
| 16 | KeepAlive Overuse | KeepAlive | Memory |
| 17 | Stream Resource Leaks | StreamResource | Memory |
| 18 | Tracked Resource Leaks | TrackedResource | Memory |
| 19 | Search + Gallery | NetworkMonitor | Network & I/O |
| 20 | Platform Channel Traffic | PlatformChannel | Network & I/O |
| 21 | Font Loading Stress | FontLoading | Network & I/O |
| 22 | Tabbed Shell | ListView, ImageMemory, LayoutBottleneck (visible tab only) | Navigation |
| 23 | Migrated App (material_ui) | CustomPainter for the app's own painter; no card names a material_ui widget |
Navigation |
| 24 | Custom Detector Cookbook | Custom (Tooltip / Slow Frame / Raster) | Custom Detectors |
| 25 | Combined: Social Feed | Image, Layout, setState, Correlator | Combined |
| 26 | Combined: Chat App | SetState, KeepAlive, Channel | Combined |
These screens record on-device capture brackets for the audit gate.
| Screen | Bracket |
|---|---|
| HeavyCompute | heavy_compute warning + critical |
| RebuildActivity | rebuild_activity warning + critical (build-time share; legs via ext.sleuthDemo.captureLeg) |
| FrameTiming (jank_detected) | jank_detected warning (60Hz) |
| FrameTiming (sustained_jank) | sustained_jank critical (60Hz) |
| GpuPressure (raster_dominance) | raster_dominance warning + critical (60Hz; counts dominant frames itself) |
| StartupDetector (slow_startup_ttff) | slow_startup_ttff warning + critical (each leg is a cold launch the host starts; the screen shows the result) |
| MemoryPressure | heap_growing warning |
| MemoryPressure (gc_pressure) | gc_pressure warning (counts young GCs itself) |
| ImageMemory (uncached_images) | uncached_images warning + critical |
| NetworkMonitor | slow_request warning + critical, large_response, request_frequency, http_error_spike and high_frequency_same_path warnings |
| PlatformChannel | platform_channel_traffic warning + critical |
| Repaint | excessive_repaint warning + critical (paint-time share; legs via ext.sleuthDemo.captureLeg) |
| StreamResource | stream_resource_growth warning |
| TrackedResource | tracked_resource_concurrent warning + tracked_resource_long_lived warning |
The RebuildActivity and Repaint screens calibrate their workload before
each leg, record a 6 s scenario, and publish results through
ext.sleuthDemo.captureResult. See the "RebuildActivity + Repaint
time-share captures" section of doc/capture_procedure.md.
Most demos explain the anti-pattern and its fix in Bad: and Fix:
lines.
Ask AI talks to a local Ollama server through its
OpenAI-compatible API (llama3.2). On a device, localhost is the device
itself, so point the app at the machine that runs Ollama:
cd example && flutter run --dart-define=SLEUTH_AI_BASE_URL=http://192.168.1.20:11434To check the chat without a model, --dart-define=SLEUTH_AI_FAKE=<mode>
swaps in a scripted adapter (lib/fake_ai_adapter.dart):
| Mode | Reply |
|---|---|
ok |
Three sentences, then done |
fail |
HTTP 503 after 300 ms (Provider error, Retry, Copy error) |
stall |
One token, then nothing (Reply stalled after 15 s) |
partial |
Two tokens, then an error (partial text kept on screen, Reply failed) |
slow |
First token after 8 s ("Still waiting for a reply" from 5 s), then as ok |
empty |
Ends without a token (Reply failed) |
The app passes FileSleuthStateStore (lib/file_state_store.dart) as
SleuthConfig.stateStore, so the trigger edge, card geometry, hidden
issues and severity filter survive restarts. On Android and iOS the file
lives in the app's own files or Library/Application Support
directory, so it also survives a new install; elsewhere it uses the
system temp directory.
The app registers service extensions for driving it from a VM service
client in debug and profile builds. Each one returns JSON, and a failure
returns {error, ...}. Lookups by text or label consider only what is on
screen: the current route, the selected tab, and an overlay page in front
of the app.
| Extension | Effect |
|---|---|
ext.sleuthDemo.open |
demo = a demo title as a slug (gpu_pressure); pushes that demo |
ext.sleuthDemo.pop |
Pops the app's top route; returns {popped} |
ext.sleuthDemo.back |
Sends a system back (as the Android back button does) and returns {handled}; with nothing open in the overlay or the app, Android leaves the app |
ext.sleuthDemo.tap |
text (substring of a Text), label (semantics label) or both x and y (logical px); scrolls the target into view (gives up after 2 s with reveal_timeout) and taps its centre, or returns obscured when something else is on top of it |
ext.sleuthDemo.type |
text, optional submit=true; types into the focused field, else the last field on screen |
ext.sleuthDemo.scroll |
Scrolls by pixels (default 600) over ms (default 600; 0 jumps), axis=horizontal for a horizontal list; picks the largest on-screen scrollable outside the demo's instructions; returns {from, to}, or {error: timeout} with at when the scroll does not finish |
ext.sleuthDemo.fling |
dx, dy (logical px), optional ms; drags and releases on that scrollable |
ext.sleuthDemo.orientation |
value = portrait | landscape | all; waits for the view to change and returns its size with settled |
ext.sleuthDemo.theme |
mode = system (Auto) | light | dark; sets the overlay's theme mode |
ext.sleuthDemo.overlay |
action = open | close | hide (first visible card; returns its key) | undo (most recently hidden key) | restoreAll | toggleSeverity (severity = critical | warning | ok) | setTheme (preset = hc_dark | hc_light | seed:<argb> | none) | menu (taps More, adds menuOpen) | notUseful (taps Not useful on the expanded card on screen, adds notUseful, or no_expanded_card); returns the state below |
ext.sleuthDemo.overlayState |
Persisted overlay state plus dashboardOpen, uiStateReady and visibleIssueCount |
ext.sleuthDemo.clipboard |
Returns {text}, the current clipboard text |
ext.sleuthDemo.a11y |
Platform accessibility settings, the overlay's text scale, overflow reports and the labelled semantics nodes |
ext.sleuthDemo.screenshot |
{png} (base64) of the whole screen, overlay included |
ext.sleuthDemo.uiBlock |
ms (1 to 5000); blocks the UI isolate for that long from a timer callback, so the VM reports the timers that fire late after it (event_loop_stall) |
ext.sleuthDemo.blurProbe |
Lists each BackdropFilter on screen with the blur sigma Sleuth reads (null when unknown), its filter class and whether it draws |
ext.sleuthDemo.retain |
bytesPerSec, optional seconds (default 60); retains a byte buffer every 50 ms on any screen, for heap_growing checks; clear=true stops and drops what it kept; returns {running, retainedBytes} |
ext.sleuthDemo.heapLegs |
floor=true|false, sample=true|false; switches heap_growing's floor and sample legs for a capture run (capture mode only) |
ext.sleuthDemo.allocationReads |
hold=true turns StreamResourceDetector off and holds Sleuth's allocation-profile reads, waiting for one in flight; hold=false restores both (capture mode only) |
ext.sleuthDemo.captureLeg, captureResult, vmAxes |
Hands-free capture legs for the time-share brackets; see doc/capture_procedure.md |
ext.sleuthDemo.rasterBurst |
frames, optional paths (default 700) and budgetUs (default 16667); draws a run of heavy frames on the open GpuPressure (raster_dominance) screen in any build and returns each frame's timing, the dominant flags and the busiest one-second count |
ext.sleuthDemo.rasterState |
The open GpuPressure (raster_dominance) screen's state: whether a leg runs, the last result and the stashed leg |
The overlay extensions return {error: no_controller} before Sleuth is
initialised. While the app is in the background no frame is drawn, so
screenshot and a11y return {error: unavailable}, and the others stop
waiting for a frame after 2 s.
Most demos use the shared DemoScaffold, which has a Before/After
toggle and a live metrics bar. Tabbed Shell, the resource-leak
screens and the Custom Detector Cookbook have their own layouts. Flip
between the anti-pattern and the fix in place, and watch Sleuth's issue
appear and disappear.
The fixed side of the toggle is a working screen, not a description, so the comparison is real. Some examples:
- Top-level
setState, fixed withValueNotifierandValueListenableBuilder. ListView(children: List.generate(...)), fixed withListView.builderanditemExtent.- An
IntrinsicHeightrow, fixed withCrossAxisAlignment.stretch(this needs a bounded cross axis, such as a fixed-height parent). Image.networkwithoutcacheWidth, fixed withcacheWidthandcacheHeight.- CSV parsing on the main isolate, fixed with
Isolate.run(). - A search request on every keystroke and 1.1 MiB gallery pages, fixed with a 300 ms debounce and 200 KB pages loaded from a button.
The live metric chips show these values: High-Level setState (bad and
fixed rebuild counts), Non-Lazy ListView (widgets built), CSV Import
(parse time on the main isolate and in an isolate), FPS Stress Test
(live FPS from addTimingsCallback), Live Waveform (paint count),
Search + Gallery (request count) and Memory Pressure (retained Dart and
native MB).
Each of these demos stacks several anti-patterns in one realistic screen, and its fixed side applies every fix together:
- Chat App has tabbed conversations with
AutomaticKeepAliveClientMixin, uncached avatars, a 40 ms platform-channel typing poll and a top-levelsetStatewhen a message arrives. - Social Feed has cards with uncached post images, an
IntrinsicHeightheader row and a top-levelsetStateon Like.
Both descriptions use the same format: a Bad line, a Fix line and a step that reproduces the issue.
- Tap the dog button to open the dashboard.
- Open a demo screen and interact with it.
- Open the dashboard again; the demo's issue cards appear in the list.
- In debug mode the example sets
enableDebugCallbacks: true, so rebuild and repaint widget highlights are visible. - Frame timing data is most accurate in profile mode.