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 banner (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. The file lives in the system
temp directory, which the OS may clear.
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 | 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); 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.