sleuth 0.30.0
sleuth: ^0.30.0 copied to clipboard
In-app performance diagnostics overlay for Flutter. Surfaces jank, memory leaks, slow networks, GPU pressure, and widget anti-patterns. Every issue ships with a fix hint.
import 'package:flutter/material.dart';
import 'package:sleuth/sleuth.dart';
import 'custom_detectors/01_simple_structural_detector.dart';
import 'custom_detectors/02_runtime_callback_detector.dart';
import 'custom_detectors/03_hybrid_vm_structural_detector.dart';
import 'demos/combined_chat_demo.dart';
import 'demos/combined_social_feed_demo.dart';
import 'demos/custom_detector_cookbook_demo.dart';
import 'demos/custom_painter_demo.dart';
import 'demos/font_loading_demo.dart';
import 'demos/fps_stress_test_demo.dart';
import 'demos/frame_timing_capture_screen.dart';
import 'demos/gpu_pressure_demo.dart';
import 'demos/heavy_compute_capture_screen.dart';
import 'demos/heavy_compute_demo.dart';
import 'demos/high_level_setstate_demo.dart';
import 'demos/intrinsic_height_demo.dart';
import 'demos/keepalive_demo.dart';
import 'demos/memory_pressure_capture_screen.dart';
import 'demos/memory_pressure_demo.dart';
import 'demos/network_monitor_capture_screen.dart';
import 'demos/network_stress_demo.dart';
import 'demos/non_lazy_list_demo.dart';
import 'demos/platform_channel_capture_screen.dart';
import 'demos/repaint_capture_screen.dart';
import 'demos/platform_channel_demo.dart';
import 'demos/rebuild_activity_capture_screen.dart';
import 'demos/rebuild_hotspot_demo.dart';
import 'demos/repaint_boundary_demo.dart';
import 'demos/repaint_stress_demo.dart';
import 'demos/stream_resource_capture_screen.dart';
import 'demos/stream_resource_demo.dart';
import 'demos/tracked_resource_capture_screen.dart';
import 'demos/tracked_resource_demo.dart';
import 'demos/shader_jank_demo.dart';
import 'demos/uncached_image_demo.dart';
void main() {
Sleuth.init();
// Capture mode gated behind a dart-define so ordinary profile-mode runs
// see no extra Timeline.instantSync traffic. Flip on for the
// runtimeVerified capture procedure:
// fvm flutter run --profile --dart-define=SLEUTH_CAPTURE_MODE=true
// Capture screens emit `sleuth.scenario.{begin,end}` +
// `sleuth.issue.<id>.<severity>` instant events while enabled.
const captureMode = bool.fromEnvironment('SLEUTH_CAPTURE_MODE');
runApp(
Sleuth.track(
child: const SleuthDemoApp(),
config: SleuthConfig(
captureMode: captureMode,
aiChat: AiChatAdapter.openAi(
apiKey: 'ollama', // Ollama ignores this but the field is required
baseUrl: 'http://localhost:11434',
model: 'llama3.2',
),
// Rebuild-detector data sources (off by default to keep the
// minimal install cheap). Both are needed for the Rebuild
// Hotspot demo — and for every other rebuild-related issue:
//
// • `enableDebugCallbacks: true` wires `debugOnRebuildDirtyWidget`
// so the detector can attribute per-type rebuild counts in
// DEBUG mode (produces `rebuild_debug_*` issue cards).
//
// • `enableDeepDebugInstrumentation: true` flips
// `debugProfileBuildsEnabledUserWidgets` + installs the
// `FlutterTimeline.debugCollect()` drain, so PROFILE mode
// populates `RouteSession.rebuildCountsByType`. That powers
// the always-on `_RebuildStatsBanner` panel on the floating
// issues card and the `RebuildStatsPage` drilldown (the
// v0.15.0 `rebuild_hotspot_summary` rollup IssueCard was
// replaced by this inline panel in v0.15.2). The
// VM-timeline `rebuild_activity` path also lights up with
// per-widget build events.
//
// Without either flag the detector has no data to evaluate,
// so no rebuild issue of any kind will ever surface — including
// the Rebuild Hotspot (Dashboard) demo.
//
// Capture-mode caveat: deep debug instrumentation flips
// `debugProfileBuildsEnabledUserWidgets`, switching BUILD events
// from sync `X` (with `dur`) to async `b/e`. `TimelineParser._isBuild`
// only registers BUILD as `PhaseEvent` when `ph == 'X'`, so
// HeavyComputeDetector goes silent and capture trace records never
// emit. Disable deep instrumentation under captureMode so BUILD
// lands as `X`.
enableDebugCallbacks: !captureMode,
enableDeepDebugInstrumentation: !captureMode,
// Cookbook custom detectors — see example/lib/custom_detectors/.
// All three are attached to the overlay so the Custom Detector
// Cookbook demo can exercise them end-to-end.
customDetectors: [
TooltipUsageDetector(),
SlowFrameDetector(),
RasterHotSpotDetector(),
],
),
),
);
}
class SleuthDemoApp extends StatelessWidget {
const SleuthDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sleuth Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF3B82F6),
useMaterial3: true,
brightness: Brightness.light,
),
darkTheme: ThemeData(
colorSchemeSeed: const Color(0xFF3B82F6),
useMaterial3: true,
brightness: Brightness.dark,
),
home: const DemoHome(),
);
}
}
// ───────────────────────────────────────────────
// Home — categorized navigation to bad-pattern demos
// ───────────────────────────────────────────────
class DemoHome extends StatelessWidget {
const DemoHome({super.key});
@override
Widget build(BuildContext context) {
final categories = <_DemoCategory>[
// ── Build ──
_DemoCategory(
title: 'Build',
icon: Icons.construction,
demos: [
_DemoRoute(
icon: Icons.refresh,
title: 'High-Level setState',
subtitle: 'Rebuild • SetStateScope detectors',
color: Colors.red,
builder: (_) => const HighLevelSetStateDemo(),
),
_DemoRoute(
icon: Icons.insights,
title: 'Rebuild Hotspot (Dashboard)',
subtitle: 'Rebuild Stats rollup + drilldown (profile)',
color: Colors.pink,
builder: (_) => const RebuildHotspotDemo(),
),
_DemoRoute(
icon: Icons.list,
title: 'Non-Lazy ListView',
subtitle: 'ListView detector',
color: Colors.orange,
builder: (_) => const NonLazyListDemo(),
),
_DemoRoute(
icon: Icons.upload_file,
title: 'CSV Import',
subtitle: 'HeavyCompute warning + critical',
color: Colors.purple,
builder: (_) => const HeavyComputeDemo(),
),
],
),
// ── Paint ──
_DemoCategory(
title: 'Paint',
icon: Icons.format_paint,
demos: [
_DemoRoute(
icon: Icons.graphic_eq,
title: 'Live Waveform',
subtitle: 'Repaint: aggregate + per-widget',
color: Colors.blueGrey,
builder: (_) => const RepaintStressDemo(),
),
_DemoRoute(
icon: Icons.brush,
title: 'Always-Repaint CustomPainter',
subtitle: 'CustomPainter detector',
color: Colors.green,
builder: (_) => const CustomPainterDemo(),
),
_DemoRoute(
icon: Icons.border_outer,
title: 'Missing RepaintBoundary',
subtitle: 'RepaintBoundary detector (structural)',
color: Colors.deepPurple,
builder: (_) => const RepaintBoundaryDemo(),
),
],
),
// ── GPU & Rendering ──
_DemoCategory(
title: 'GPU & Rendering',
icon: Icons.layers,
demos: [
_DemoRoute(
icon: Icons.memory_outlined,
title: 'GPU Pressure',
subtitle: 'GpuPressure detector (hybrid)',
color: Colors.deepOrange,
builder: (_) => const GpuPressureDemo(),
),
_DemoRoute(
icon: Icons.blur_on,
title: 'Shader Jank',
subtitle: 'ShaderJank detector (Skia only)',
color: Colors.indigo,
builder: (_) => const ShaderJankDemo(),
),
_DemoRoute(
icon: Icons.local_fire_department,
title: 'FPS Stress Test (~20 FPS)',
subtitle: 'Heavy compute + GPU blur every frame',
color: Colors.red,
builder: (_) => const FpsStressTestDemo(),
),
],
),
// ── Layout ──
_DemoCategory(
title: 'Layout',
icon: Icons.grid_on,
demos: [
_DemoRoute(
icon: Icons.height,
title: 'IntrinsicHeight Abuse',
subtitle: 'LayoutBottleneck detector',
color: Colors.amber,
builder: (_) => const IntrinsicHeightDemo(),
),
],
),
// ── Memory ──
_DemoCategory(
title: 'Memory',
icon: Icons.memory,
demos: [
_DemoRoute(
icon: Icons.image,
title: 'Uncached Images',
subtitle: 'ImageMemory detector',
color: Colors.teal,
builder: (_) => const UncachedImageDemo(),
),
_DemoRoute(
icon: Icons.data_array,
title: 'Memory Pressure',
subtitle: 'MemoryPressure detector (VM-only)',
color: Colors.purple,
builder: (_) => const MemoryPressureDemo(),
),
_DemoRoute(
icon: Icons.all_inclusive,
title: 'KeepAlive Overuse',
subtitle: 'KeepAlive detector (>5 alive)',
color: Colors.pink,
builder: (_) => const KeepAliveDemo(),
),
_DemoRoute(
icon: Icons.stream,
title: 'Stream Resource Leaks',
subtitle: 'StreamResource detector (Timer + Controller leaks)',
color: Colors.deepPurple,
builder: (_) => const StreamResourceDemo(),
),
_DemoRoute(
icon: Icons.bookmark_added,
title: 'Tracked Resource Leaks',
subtitle: 'Sleuth.trackResource opt-in retention tracking',
color: Colors.indigo,
builder: (_) => const TrackedResourceDemo(),
),
],
),
// ── Network & I/O ──
_DemoCategory(
title: 'Network & I/O',
icon: Icons.cloud,
demos: [
_DemoRoute(
icon: Icons.search,
title: 'Search + Gallery',
subtitle: 'NetworkMonitor: slow / frequency / large',
color: Colors.orange,
builder: (_) => const NetworkStressDemo(),
),
_DemoRoute(
icon: Icons.settings_input_hdmi,
title: 'Platform Channel Traffic',
subtitle: 'PlatformChannel detector (>20/sec)',
color: Colors.blueGrey,
builder: (_) => const PlatformChannelDemo(),
),
_DemoRoute(
icon: Icons.font_download,
title: 'Font Loading Stress',
subtitle: 'FontLoading detector (>3 custom fonts)',
color: Colors.deepOrange,
builder: (_) => const FontLoadingDemo(),
),
],
),
// ── Custom Detectors ──
_DemoCategory(
title: 'Custom Detectors',
icon: Icons.extension,
demos: [
_DemoRoute(
icon: Icons.extension_outlined,
title: 'Custom Detector Cookbook',
subtitle: 'Tooltip • Slow frame • Raster hot spot (cookbook)',
color: Colors.deepPurple,
builder: (_) => const CustomDetectorCookbookDemo(),
),
],
),
// ── Combined ──
_DemoCategory(
title: 'Combined',
icon: Icons.dashboard,
demos: [
_DemoRoute(
icon: Icons.dynamic_feed,
title: 'Combined: Social Feed',
subtitle: 'Image • Layout • setState • Correlator',
color: Colors.deepPurple,
builder: (_) => const CombinedSocialFeedDemo(),
),
_DemoRoute(
icon: Icons.chat,
title: 'Combined: Chat App',
subtitle: 'Rebuild + KeepAlive + Channel + SetState',
color: Colors.blue,
builder: (_) => const CombinedChatDemo(),
),
],
),
// ── Capture Helpers ──
// Operator-only tooling for the runtimeVerified bracket-recording
// procedure (see doc/capture_procedure.md). Each helper drives
// Sleuth.markScenarioBegin/End around a workload at known magnitude
// (below / at / above the bracket band) so detectors emit captured
// trace records on real devices.
_DemoCategory(
title: 'Capture Helpers',
icon: Icons.videocam,
demos: [
_DemoRoute(
icon: Icons.speed,
title: 'HeavyCompute',
subtitle: 'heavy_compute warning + critical brackets',
color: Colors.purple,
builder: (_) => const HeavyComputeCaptureScreen(),
),
_DemoRoute(
icon: Icons.refresh,
title: 'RebuildActivity',
subtitle: 'rebuild_activity warning + critical brackets',
color: Colors.teal,
builder: (_) => const RebuildActivityCaptureScreen(),
),
_DemoRoute(
icon: Icons.timeline,
title: 'FrameTiming (jank_detected)',
subtitle: 'jank_detected warning bracket (60Hz)',
color: Colors.indigo,
builder: (_) => const FrameTimingCaptureScreen(),
),
_DemoRoute(
icon: Icons.data_array,
title: 'MemoryPressure',
subtitle: 'heap_growing warning bracket',
color: Colors.purple,
builder: (_) => const MemoryPressureCaptureScreen(),
),
_DemoRoute(
icon: Icons.cloud_download,
title: 'NetworkMonitor',
subtitle: 'slow_request warning + critical brackets',
color: Colors.orange,
builder: (_) => const NetworkMonitorCaptureScreen(),
),
_DemoRoute(
icon: Icons.settings_input_hdmi,
title: 'PlatformChannel',
subtitle: 'platform_channel_traffic warning bracket',
color: Colors.blueGrey,
builder: (_) => const PlatformChannelCaptureScreen(),
),
_DemoRoute(
icon: Icons.brush,
title: 'Repaint',
subtitle: 'excessive_repaint warning bracket',
color: Colors.pink,
builder: (_) => const RepaintCaptureScreen(),
),
_DemoRoute(
icon: Icons.stream,
title: 'StreamResource',
subtitle: 'stream_resource_growth warning bracket',
color: Colors.deepPurple,
builder: (_) => const StreamResourceCaptureScreen(),
),
_DemoRoute(
icon: Icons.track_changes,
title: 'TrackedResource',
subtitle: 'tracked_resource_concurrent warning bracket',
color: Colors.teal,
builder: (_) => const TrackedResourceCaptureScreen(),
),
],
),
];
return Scaffold(
appBar: AppBar(
title: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.pets, size: 20),
SizedBox(width: 8),
Text('Sleuth Demo'),
],
),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: categories.fold<int>(
0,
(sum, c) => sum + 1 + c.demos.length,
),
itemBuilder: (context, index) {
// Map flat index to category header or demo tile.
var remaining = index;
for (final category in categories) {
if (remaining == 0) {
return _CategoryHeader(
title: category.title,
icon: category.icon,
);
}
remaining--;
if (remaining < category.demos.length) {
final demo = category.demos[remaining];
return _DemoTile(demo: demo);
}
remaining -= category.demos.length;
}
return const SizedBox.shrink();
},
),
);
}
}
// ── Category header ──
class _CategoryHeader extends StatelessWidget {
const _CategoryHeader({required this.title, required this.icon});
final String title;
final IconData icon;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Row(
children: [
Icon(icon, size: 18, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
);
}
}
// ── Demo tile ──
class _DemoTile extends StatelessWidget {
const _DemoTile({required this.demo});
final _DemoRoute demo;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: CircleAvatar(
backgroundColor: demo.color.withValues(alpha: 0.15),
child: Icon(demo.icon, color: demo.color),
),
title: Text(
demo.title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
demo.subtitle,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: '/demo/${demo.title}'),
builder: demo.builder,
),
),
),
),
);
}
}
// ── Data classes ──
class _DemoCategory {
const _DemoCategory({
required this.title,
required this.icon,
required this.demos,
});
final String title;
final IconData icon;
final List<_DemoRoute> demos;
}
class _DemoRoute {
const _DemoRoute({
required this.icon,
required this.title,
required this.subtitle,
required this.color,
required this.builder,
});
final IconData icon;
final String title;
final String subtitle;
final Color color;
final WidgetBuilder builder;
}