memory leak tracking & fixes

This commit is contained in:
Thibault Deckers 2023-10-20 22:44:15 +03:00
parent bca78a0669
commit c5fde95c73
19 changed files with 118 additions and 40 deletions

View file

@ -4,7 +4,8 @@ class AnalysisController {
final bool canStartService, force;
final int progressTotal, progressOffset;
final List<int>? entryIds;
final ValueNotifier<bool> stopSignal;
final ValueNotifier<bool> _stopSignal = ValueNotifier(false);
AnalysisController({
this.canStartService = true,
@ -12,8 +13,24 @@ class AnalysisController {
this.force = false,
this.progressTotal = 0,
this.progressOffset = 0,
ValueNotifier<bool>? stopSignal,
}) : stopSignal = stopSignal ?? ValueNotifier(false);
}) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AnalysisController',
object: this,
);
}
}
bool get isStopping => stopSignal.value;
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_stopSignal.dispose();
}
bool get isStopping => _stopSignal.value;
void enableStopSignal() => _stopSignal.value = true;
}

View file

@ -61,6 +61,13 @@ mixin SourceBase {
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin {
CollectionSource() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$CollectionSource',
object: this,
);
}
settings.updateStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => invalidateAlbumDisplayNames());
settings.updateStream.where((event) => event.key == SettingKeys.hiddenFiltersKey).listen((event) {
final oldValue = event.oldValue;
@ -76,6 +83,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
});
}
@mustCallSuper
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_rawEntries.forEach((v) => v.dispose());
}
final EventBus _eventBus = EventBus();
@override
@ -447,7 +462,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
Future<void> analyze(AnalysisController? analysisController, {Set<AvesEntry>? entries}) async {
final todoEntries = entries ?? visibleEntries;
final _analysisController = analysisController ?? AnalysisController();
final defaultAnalysisController = AnalysisController();
final _analysisController = analysisController ?? defaultAnalysisController;
final force = _analysisController.force;
if (!_analysisController.isStopping) {
var startAnalysisService = false;
@ -481,6 +497,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
updateDerivedFilters(todoEntries);
}
}
defaultAnalysisController.dispose();
state = SourceState.ready;
}

View file

@ -109,9 +109,11 @@ class Analyzer {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_stopUpdateTimer();
_controller?.dispose();
_serviceStateNotifier.removeListener(_onServiceStateChanged);
_source.stateNotifier.removeListener(_onSourceStateChanged);
_stopUpdateTimer();
_source.dispose();
}
Future<void> start(dynamic args) async {
@ -125,13 +127,13 @@ class Analyzer {
progressOffset = args['progressOffset'];
}
debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
_controller?.dispose();
_controller = AnalysisController(
canStartService: false,
entryIds: entryIds,
force: force,
progressTotal: progressTotal,
progressOffset: progressOffset,
stopSignal: ValueNotifier(false),
);
settings.systemLocalesFallback = await deviceService.getLocales();
@ -160,7 +162,7 @@ class Analyzer {
await _stopPlatformService();
_serviceStateNotifier.value = AnalyzerState.stopped;
case AnalyzerState.stopped:
_controller?.stopSignal.value = true;
_controller?.enableStopSignal();
_stopUpdateTimer();
}
}

View file

@ -90,5 +90,6 @@ Future<AvesEntry?> _getWidgetEntry(int widgetId, bool reuseEntry) async {
if (entry != null) {
settings.setWidgetUri(widgetId, entry.uri);
}
source.dispose();
return entry;
}

View file

@ -196,13 +196,14 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override
void dispose() {
_pageTransitionsBuilderNotifier.dispose();
_tvMediaQueryModifierNotifier.dispose();
_appModeNotifier.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
WidgetsBinding.instance.removeObserver(this);
_pageTransitionsBuilderNotifier.dispose();
_tvMediaQueryModifierNotifier.dispose();
_appModeNotifier.dispose();
_mediaStoreSource.dispose();
super.dispose();
}

View file

@ -257,7 +257,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
final controller = AnalysisController(canStartService: true, force: true);
final collection = context.read<CollectionLens>();
collection.source.analyze(controller, entries: entries);
collection.source.analyze(controller, entries: entries).then((_) => controller.dispose());
_browse(context);
}

View file

@ -89,9 +89,11 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
}
void _clearChooserOverlayEntry() {
if (_chooserOverlayEntry != null) {
_chooserOverlayEntry!.remove();
_chooserOverlayEntry = null;
final overlayEntry = _chooserOverlayEntry;
_chooserOverlayEntry = null;
if (overlayEntry != null) {
overlayEntry.remove();
overlayEntry.dispose();
}
}

View file

@ -180,9 +180,12 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
void _onScaleEnd(ScaleEndDetails details) {
if (_scaledSizeNotifier == null) return;
if (_overlayEntry != null) {
_overlayEntry!.remove();
_overlayEntry = null;
final overlayEntry = _overlayEntry;
_overlayEntry = null;
if (overlayEntry != null) {
overlayEntry.remove();
overlayEntry.dispose();
}
_applyingScale = true;

View file

@ -38,6 +38,12 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
_startDbReport();
}
@override
void dispose() {
_disposeLoadedContent();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -63,7 +69,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.reset().then((_) => _startDbReport()),
onPressed: () => metadataDb.reset().then((_) => _reload()),
child: const Text('Reset'),
),
],
@ -86,7 +92,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearEntries().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearEntries().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -107,7 +113,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearDates().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -128,7 +134,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -149,7 +155,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearAddresses().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearAddresses().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -170,7 +176,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearTrashDetails().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearTrashDetails().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -191,7 +197,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => vaults.clear().then((_) => _startDbReport()),
onPressed: () => vaults.clear().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -212,7 +218,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => favourites.clear().then((_) => _startDbReport()),
onPressed: () => favourites.clear().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -233,7 +239,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => covers.clear().then((_) => _startDbReport()),
onPressed: () => covers.clear().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -254,7 +260,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => metadataDb.clearVideoPlayback().then((_) => _startDbReport()),
onPressed: () => metadataDb.clearVideoPlayback().then((_) => _reload()),
child: const Text('Clear'),
),
],
@ -268,6 +274,11 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
);
}
Future<void> _reload() async {
await _disposeLoadedContent();
_startDbReport();
}
void _startDbReport() {
_dbFileSizeLoader = metadataDb.dbFileSize();
_dbEntryLoader = metadataDb.loadEntries();
@ -282,6 +293,10 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
setState(() {});
}
Future<void> _disposeLoadedContent() async {
(await _dbEntryLoader).forEach((v) => v.dispose());
}
@override
bool get wantKeepAlive => true;
}

View file

@ -47,14 +47,17 @@ class _DebugGeneralSectionState extends State<DebugGeneralSection> with Automati
SwitchListTile(
value: _taskQueueOverlayEntry != null,
onChanged: (v) {
_taskQueueOverlayEntry?.remove();
final overlayEntry = _taskQueueOverlayEntry;
_taskQueueOverlayEntry = null;
if (overlayEntry != null) {
overlayEntry.remove();
overlayEntry.dispose();
}
if (v) {
_taskQueueOverlayEntry = OverlayEntry(
builder: (context) => const DebugTaskQueueOverlay(),
);
Overlay.of(context).insert(_taskQueueOverlayEntry!);
} else {
_taskQueueOverlayEntry = null;
}
setState(() {});
},

View file

@ -163,7 +163,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
_overlayAnimationController.dispose();
_overlayVisible.removeListener(_onOverlayVisibleChanged);
_mapController.dispose();
_selectedIndexNotifier.removeListener(_onThumbnailIndexChanged);
_selectedIndexNotifier.dispose();
regionCollection?.dispose();
// provided collection should be a new instance specifically created
// for the `MapPage` widget, so it can be safely disposed here

View file

@ -49,7 +49,7 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
@override
void dispose() {
_entryIndexNotifier.removeListener(_onScrollerIndexChanged);
_entryIndexNotifier.dispose();
super.dispose();
}

View file

@ -33,11 +33,10 @@ class VideoConductor {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
await _disposeAll();
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
_controllers.forEach((v) => v.dispose());
await _disposeAll();
_controllers.clear();
if (settings.keepScreenOn == KeepScreenOn.videoPlayback) {
await windowService.keepScreenOn(false);

View file

@ -73,6 +73,10 @@ class ViewStateConductor {
entry,
...?entry.burstEntries,
}.map((v) => v.uri).toSet();
_controllers.removeWhere((v) => uris.contains(v.entry.uri));
final entryControllers = _controllers.where((v) => uris.contains(v.entry.uri)).toSet();
entryControllers.forEach((controller) {
_controllers.remove(controller);
controller.dispose();
});
}
}

View file

@ -29,5 +29,6 @@ class ViewStateController with HistogramMixin {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
viewStateNotifier.dispose();
fullImageNotifier.dispose();
}
}

View file

@ -84,6 +84,7 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
void dispose() {
_unregisterWidget(widget);
widget.onDisposed?.call();
_actionFeedbackChildNotifier.dispose();
super.dispose();
}
@ -305,9 +306,11 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
return true;
};
onScaleEnd = (details) {
if (_actionFeedbackOverlayEntry != null) {
_actionFeedbackOverlayEntry!.remove();
_actionFeedbackOverlayEntry = null;
final overlayEntry = _actionFeedbackOverlayEntry;
_actionFeedbackOverlayEntry = null;
if (overlayEntry != null) {
overlayEntry.remove();
overlayEntry.dispose();
}
};
}

View file

@ -79,6 +79,7 @@ class _VideoCoverState extends State<VideoCover> {
@override
void dispose() {
_unregisterWidget(widget);
_videoCoverInfoNotifier.dispose();
super.dispose();
}

View file

@ -37,6 +37,12 @@ class MagnifierGestureDetector extends StatefulWidget {
class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
final ValueNotifier<TapDownDetails?> doubleTapDetails = ValueNotifier(null);
@override
void dispose() {
doubleTapDetails.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final gestureSettings = MediaQuery.gestureSettingsOf(context);

View file

@ -12,7 +12,7 @@ import 'package:media_kit_video/media_kit_video.dart';
class MpvVideoController extends AvesVideoController {
late Player _instance;
late VideoStatus _status;
bool _firstFrameRendered = false;
bool _disposed = false, _firstFrameRendered = false;
final ValueNotifier<VideoController?> _controllerNotifier = ValueNotifier(null);
final List<StreamSubscription> _subscriptions = [];
final StreamController<VideoStatus> _statusStreamController = StreamController.broadcast();
@ -63,12 +63,15 @@ class MpvVideoController extends AvesVideoController {
@override
Future<void> dispose() async {
assert(!_disposed);
_disposed = true;
await super.dispose();
_stopListening();
_stopStreamFetchTimer();
await _statusStreamController.close();
await _timedTextStreamController.close();
await _instance.dispose();
_controllerNotifier.dispose();
_completedNotifier.dispose();
}