From 509c40222718799ca584682e465c1a815a34e93d Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 18 Mar 2025 23:41:39 +0100 Subject: [PATCH] debug: leak tracking for `GCed late` and `not GCed` --- lib/widgets/debug/app_debug_page.dart | 2 + lib/widgets/debug/general.dart | 89 ++++++++--------- lib/widgets/debug/leaking.dart | 137 ++++++++++++++++++++++++++ lib/widgets/debug/overlay.dart | 44 --------- 4 files changed, 184 insertions(+), 88 deletions(-) create mode 100644 lib/widgets/debug/leaking.dart delete mode 100644 lib/widgets/debug/overlay.dart diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index f124ce6ae..9c0b74376 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -18,6 +18,7 @@ import 'package:aves/widgets/debug/capabilities.dart'; import 'package:aves/widgets/debug/colors.dart'; import 'package:aves/widgets/debug/database.dart'; import 'package:aves/widgets/debug/general.dart'; +import 'package:aves/widgets/debug/leaking.dart'; import 'package:aves/widgets/debug/media_store_scan_dialog.dart'; import 'package:aves/widgets/debug/os_apps.dart'; import 'package:aves/widgets/debug/os_codecs.dart'; @@ -73,6 +74,7 @@ class AppDebugPage extends StatelessWidget { padding: const EdgeInsets.all(8), children: const [ DebugGeneralSection(), + DebugLeakingSection(), DebugCacheSection(), DebugCapabilitiesSection(), DebugColorSection(), diff --git a/lib/widgets/debug/general.dart b/lib/widgets/debug/general.dart index 985dc20f3..458c9fcf1 100644 --- a/lib/widgets/debug/general.dart +++ b/lib/widgets/debug/general.dart @@ -1,14 +1,12 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/analysis_service.dart'; +import 'package:aves/services/common/service_policy.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/debug/overlay.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/viewer/info/common.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:leak_tracker/leak_tracker.dart'; import 'package:provider/provider.dart'; class DebugGeneralSection extends StatefulWidget { @@ -31,6 +29,7 @@ class _DebugGeneralSectionState extends State with Automati final withGps = catalogued.where((entry) => entry.hasGps); final withAddress = withGps.where((entry) => entry.hasAddress); final withFineAddress = withGps.where((entry) => entry.hasFineAddress); + return AvesExpansionTile( title: 'General', children: [ @@ -55,7 +54,7 @@ class _DebugGeneralSectionState extends State with Automati _taskQueueOverlayEntry = null; if (v) { _taskQueueOverlayEntry = OverlayEntry( - builder: (context) => const DebugTaskQueueOverlay(), + builder: (context) => const _TaskQueueOverlay(), ); Overlay.of(context).insert(_taskQueueOverlayEntry!); } @@ -68,46 +67,6 @@ class _DebugGeneralSectionState extends State with Automati onChanged: (v) => settings.debugShowViewerTiles = v, title: 'Show viewer tiles', ), - ElevatedButton( - onPressed: () => LeakTracking.collectLeaks().then((leaks) { - const config = LeakDiagnosticConfig( - collectRetainingPathForNotGCed: true, - collectStackTraceOnStart: true, - collectStackTraceOnDisposal: true, - ); - LeakTracking.phase = const PhaseSettings( - leakDiagnosticConfig: config, - ); - debugPrint('Setup leak tracking phase with config=$config'); - }), - child: const Text('Setup leak tracking phase'), - ), - ElevatedButton( - onPressed: () => LeakTracking.collectLeaks().then((leaks) { - leaks.byType.forEach((type, reports) { - // ignore `notGCed` and `gcedLate` for now - if (type != LeakType.notDisposed) return; - - debugPrint('* leak type=$type'); - groupBy(reports, (report) => report.type).forEach((reportType, typedReports) { - debugPrint(' * report type=$reportType'); - groupBy(typedReports, (report) => report.trackedClass).forEach((trackedClass, classedReports) { - debugPrint(' trackedClass=$trackedClass reports=${classedReports.length}'); - classedReports.forEach((report) { - final phase = report.phase; - final retainingPath = report.retainingPath; - final detailedPath = report.detailedPath; - final context = report.context; - if (phase != null || retainingPath != null || detailedPath != null || context != null) { - debugPrint(' phase=$phase retainingPath=$retainingPath detailedPath=$detailedPath context=$context'); - } - }); - }); - }); - }); - }), - child: const Text('Collect leaks'), - ), ElevatedButton( onPressed: () => AnalysisService.startService(force: false), child: const Text('Start analysis service'), @@ -133,3 +92,45 @@ class _DebugGeneralSectionState extends State with Automati @override bool get wantKeepAlive => true; } + +class _TaskQueueOverlay extends StatelessWidget { + const _TaskQueueOverlay(); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: DefaultTextStyle( + style: const TextStyle(), + child: Align( + alignment: AlignmentDirectional.bottomStart, + child: SafeArea( + child: Container( + color: Colors.indigo.shade900.withAlpha(0xCC), + padding: const EdgeInsets.all(8), + child: StreamBuilder( + stream: servicePolicy.queueStream, + builder: (context, snapshot) { + if (snapshot.hasError) return const SizedBox(); + final queuedEntries = >[]; + if (snapshot.hasData) { + final state = snapshot.data!; + queuedEntries.add(MapEntry('run', state.runningCount)); + queuedEntries.add(MapEntry('paused', state.pausedCount)); + queuedEntries.addAll(state.queueByPriority.entries.map((kv) => MapEntry(kv.key.toString(), kv.value))); + } + queuedEntries.sort((a, b) => a.key.compareTo(b.key)); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(queuedEntries.map((kv) => '${kv.key}: ${kv.value}').join(', ')), + ], + ); + }), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/debug/leaking.dart b/lib/widgets/debug/leaking.dart new file mode 100644 index 000000000..2f03f0036 --- /dev/null +++ b/lib/widgets/debug/leaking.dart @@ -0,0 +1,137 @@ +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:leak_tracker/leak_tracker.dart'; + +class DebugLeakingSection extends StatefulWidget { + const DebugLeakingSection({super.key}); + + @override + State createState() => _DebugLeakingSectionState(); +} + +class _DebugLeakingSectionState extends State with AutomaticKeepAliveClientMixin { + static OverlayEntry? _collectorOverlayEntry; + + static const _leakIgnoreConfig = IgnoredLeaks( + experimentalNotGCed: IgnoredLeaksSet(), + notDisposed: IgnoredLeaksSet(), + ); + + @override + Widget build(BuildContext context) { + super.build(context); + + return AvesExpansionTile( + title: 'Leaking', + children: [ + SwitchListTile( + value: _collectorOverlayEntry != null, + onChanged: (v) { + _collectorOverlayEntry + ?..remove() + ..dispose(); + _collectorOverlayEntry = null; + if (v) { + _collectorOverlayEntry = OverlayEntry( + builder: (context) => const _CollectorOverlay(), + ); + Overlay.of(context).insert(_collectorOverlayEntry!); + } + setState(() {}); + }, + title: const Text('Show leak report overlay'), + ), + ElevatedButton( + onPressed: () => LeakTracking.collectLeaks().then((leaks) { + LeakTracking.phase = const PhaseSettings( + ignoredLeaks: _leakIgnoreConfig, + leakDiagnosticConfig: LeakDiagnosticConfig( + collectRetainingPathForNotGCed: true, + collectStackTraceOnStart: true, + collectStackTraceOnDisposal: true, + ), + ); + }), + child: const Text('Track leaks with stacks'), + ), + ElevatedButton( + onPressed: () => LeakTracking.collectLeaks().then((leaks) { + LeakTracking.phase = const PhaseSettings( + ignoredLeaks: _leakIgnoreConfig, + leakDiagnosticConfig: LeakDiagnosticConfig( + collectRetainingPathForNotGCed: true, + collectStackTraceOnStart: false, + collectStackTraceOnDisposal: false, + ), + ); + }), + child: const Text('Track leaks without stacks'), + ), + ], + ); + } + + @override + bool get wantKeepAlive => true; +} + +class _CollectorOverlay extends StatefulWidget { + const _CollectorOverlay(); + + @override + State<_CollectorOverlay> createState() => _CollectorOverlayState(); +} + +class _CollectorOverlayState extends State<_CollectorOverlay> { + AlignmentGeometry _alignment = AlignmentDirectional.bottomStart; + + @override + Widget build(BuildContext context) { + return DefaultTextStyle( + style: const TextStyle(), + child: Align( + alignment: _alignment, + child: SafeArea( + child: Container( + color: Colors.indigo.shade900.withAlpha(0xCC), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + onPressed: () => setState(() => _alignment = _alignment == AlignmentDirectional.bottomStart ? AlignmentDirectional.topStart : AlignmentDirectional.bottomStart), + icon: Icon(_alignment == AlignmentDirectional.bottomStart ? Icons.vertical_align_top_outlined : Icons.vertical_align_bottom_outlined), + ), + ...LeakType.values.map((type) { + return TextButton( + onPressed: () => LeakTracking.collectLeaks().then((leaks) { + final reports = leaks.byType[type] ?? []; + _printLeaks(type, reports); + }), + child: Text(type.name), + ); + }) + ]), + ), + ), + ), + ); + } + + void _printLeaks(LeakType type, List reports) { + debugPrint('* leak type=$type, ${reports.length} reports'); + groupBy(reports, (report) => report.type).forEach((reportType, typedReports) { + debugPrint(' * report type=$reportType'); + groupBy(typedReports, (report) => report.trackedClass).forEach((trackedClass, classedReports) { + debugPrint(' trackedClass=$trackedClass reports=${classedReports.length}'); + classedReports.forEach((report) { + final phase = report.phase; + final retainingPath = report.retainingPath; + final detailedPath = report.detailedPath; + final context = report.context; + if (phase != null || retainingPath != null || detailedPath != null || context != null) { + debugPrint(' phase=$phase retainingPath=$retainingPath detailedPath=$detailedPath context=$context'); + } + }); + }); + }); + } +} diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart deleted file mode 100644 index 6011989a5..000000000 --- a/lib/widgets/debug/overlay.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:aves/services/common/service_policy.dart'; -import 'package:flutter/material.dart'; - -class DebugTaskQueueOverlay extends StatelessWidget { - const DebugTaskQueueOverlay({super.key}); - - @override - Widget build(BuildContext context) { - return IgnorePointer( - child: DefaultTextStyle( - style: const TextStyle(), - child: Align( - alignment: AlignmentDirectional.bottomStart, - child: SafeArea( - child: Container( - color: Colors.indigo.shade900.withAlpha(0xCC), - padding: const EdgeInsets.all(8), - child: StreamBuilder( - stream: servicePolicy.queueStream, - builder: (context, snapshot) { - if (snapshot.hasError) return const SizedBox(); - final queuedEntries = >[]; - if (snapshot.hasData) { - final state = snapshot.data!; - queuedEntries.add(MapEntry('run', state.runningCount)); - queuedEntries.add(MapEntry('paused', state.pausedCount)); - queuedEntries.addAll(state.queueByPriority.entries.map((kv) => MapEntry(kv.key.toString(), kv.value))); - } - queuedEntries.sort((a, b) => a.key.compareTo(b.key)); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(queuedEntries.map((kv) => '${kv.key}: ${kv.value}').join(', ')), - ], - ); - }), - ), - ), - ), - ), - ); - } -}