diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c8b8515..a24a81cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ All notable changes to this project will be documented in this file. - disabling animations also applies to pop up menus - upgraded Flutter to stable v3.19.3 +### Fixed + +- engine leak from analysis worker + ## [v1.10.5] - 2024-02-22 ### Added diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt index ce05c1da3..b9ce72658 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/AnalysisWorker.kt @@ -14,7 +14,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import app.loup.streams_channel.StreamsChannel -import deckers.thibault.aves.channel.calls.DebugHandler import deckers.thibault.aves.channel.calls.DeviceHandler import deckers.thibault.aves.channel.calls.GeocodingHandler import deckers.thibault.aves.channel.calls.MediaStoreHandler @@ -98,7 +97,6 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine // dart -> platform -> dart // - need Context - MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(context)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 75b3544f8..3292bb715 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -15,12 +15,15 @@ import android.util.Log import androidx.exifinterface.media.ExifInterface import com.drew.metadata.file.FileTypeDirectory import deckers.thibault.aves.channel.calls.Coresult.Companion.safe -import deckers.thibault.aves.metadata.* +import deckers.thibault.aves.metadata.ExifInterfaceHelper +import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper +import deckers.thibault.aves.metadata.Metadata +import deckers.thibault.aves.metadata.Mp4ParserHelper import deckers.thibault.aves.metadata.Mp4ParserHelper.dumpBoxes +import deckers.thibault.aves.metadata.PixyMetaHelper import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.LogUtils -import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor @@ -53,7 +56,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler { "exceptionInCoroutine" -> ioScope.launch { throw TestException() } "safeExceptionInCoroutine" -> ioScope.launch { safe(call, result) { _, _ -> throw TestException() } } - "getAvailableHeapSize" -> safe(call, result, ::getAvailableHeapSize) "getContextDirs" -> ioScope.launch { safe(call, result, ::getContextDirs) } "getCodecs" -> safe(call, result, ::getCodecs) "getEnv" -> safe(call, result, ::getEnv) @@ -70,10 +72,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler { } } - private fun getAvailableHeapSize(@Suppress("unused_parameter") methodCall: MethodCall, result: MethodChannel.Result) { - result.success(MemoryUtils.getAvailableHeapSize()) - } - private fun getContextDirs(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { val dirs = hashMapOf( "cacheDir" to context.cacheDir, diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index 6693112dd..3b1306a99 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -12,7 +12,7 @@ import androidx.core.content.pm.ShortcutManagerCompat import com.google.android.material.color.DynamicColors import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.model.FieldMap -import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.MemoryUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -35,6 +35,8 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled) "requestMediaManagePermission" -> safe(call, result, ::requestMediaManagePermission) + "getAvailableHeapSize" -> safe(call, result, ::getAvailableHeapSize) + "requestGarbageCollection" -> safe(call, result, ::requestGarbageCollection) else -> result.notImplemented() } } @@ -123,6 +125,15 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { result.success(true) } + private fun getAvailableHeapSize(@Suppress("unused_parameter") methodCall: MethodCall, result: MethodChannel.Result) { + result.success(MemoryUtils.getAvailableHeapSize()) + } + + private fun requestGarbageCollection(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + Runtime.getRuntime().gc() + result.success(true) + } + companion object { const val CHANNEL = "deckers.thibault/aves/device" } diff --git a/lib/model/entry/extensions/catalog.dart b/lib/model/entry/extensions/catalog.dart index 626d55e32..94dd3b148 100644 --- a/lib/model/entry/extensions/catalog.dart +++ b/lib/model/entry/extensions/catalog.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/geotiff.dart'; @@ -6,10 +8,14 @@ import 'package:aves/model/video/metadata.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; +import 'package:flutter/foundation.dart'; extension ExtraAvesEntryCatalog on AvesEntry { Future catalog({required bool background, required bool force, required bool persist}) async { if (isCatalogued && !force) return; + + final beforeAvailableHeapSize = await deviceService.getAvailableHeapSize(); + if (isSvg) { // vector image sizing is not essential, so we should not spend time for it during loading // but it is useful anyway (for aspect ratios etc.) so we size them during cataloguing @@ -53,5 +59,14 @@ extension ExtraAvesEntryCatalog on AvesEntry { } } } + + final afterAvailableHeapSize = await deviceService.getAvailableHeapSize(); + final diff = beforeAvailableHeapSize - afterAvailableHeapSize; + const largeHeapUsageThreshold = 15 * (1 << 20); // MB + + if (diff > largeHeapUsageThreshold) { + debugPrint('Large heap usage (${diff}B) from cataloguing entry=$this size=$sizeBytes'); + await deviceService.requestGarbageCollection(); + } } } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index c6a4216b2..a198cc66e 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -449,6 +449,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place } if (dataTypes.contains(EntryDataType.catalog)) { + // explicit GC before cataloguing multiple items + await deviceService.requestGarbageCollection(); await Future.forEach(entries, (entry) async { await entry.catalog(background: background, force: dataTypes.contains(EntryDataType.catalog), persist: persist); await metadataDb.updateCatalogMetadata(entry.id, entry.catalogMetadata); @@ -499,6 +501,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place entryIds: entries?.map((entry) => entry.id).toList(), ); } else { + // explicit GC before cataloguing multiple items + await deviceService.requestGarbageCollection(); await catalogEntries(_analysisController, todoEntries); updateDerivedFilters(todoEntries); await locateEntries(_analysisController, todoEntries); diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index 33b352fe0..9acb772da 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -46,16 +46,6 @@ class AndroidDebugService { } } - static Future getAvailableHeapSize() async { - try { - final result = await _platform.invokeMethod('getAvailableHeapSize'); - if (result != null) return result as int; - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - return 0; - } - static Future getContextDirs() async { try { final result = await _platform.invokeMethod('getContextDirs'); diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart index 03ea12764..26500505b 100644 --- a/lib/services/device_service.dart +++ b/lib/services/device_service.dart @@ -17,6 +17,10 @@ abstract class DeviceService { Future isSystemFilePickerEnabled(); Future requestMediaManagePermission(); + + Future getAvailableHeapSize(); + + Future requestGarbageCollection(); } class PlatformDeviceService implements DeviceService { @@ -104,4 +108,24 @@ class PlatformDeviceService implements DeviceService { await reportService.recordError(e, stack); } } + + @override + Future getAvailableHeapSize() async { + try { + final result = await _platform.invokeMethod('getAvailableHeapSize'); + if (result != null) return result as int; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return 0; + } + + @override + Future requestGarbageCollection() async { + try { + await _platform.invokeMethod('requestGarbageCollection'); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + } }