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