sleuth 0.30.0 copy "sleuth: ^0.30.0" to clipboard
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.

example/lib/main.dart

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;
}
2
likes
150
points
123
downloads
screenshot

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues
Contributing

Topics

#performance #debugging #developer-tools #profiling

License

MIT (license)

Dependencies

flutter, meta, path, vm_service

More

Packages that depend on sleuth