diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7b9ba35269..0aae1ce1b8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -120,16 +120,6 @@ android:exported="false" tools:node="remove"/> - - - - Twake Mail - Started - In Progress - Canceled - Failed - Completed - Paused \ No newline at end of file diff --git a/docs/adr/0079-reduce-twake-mail-mobile-memory-usage.md b/docs/adr/0079-reduce-twake-mail-mobile-memory-usage.md new file mode 100644 index 0000000000..c39f567200 --- /dev/null +++ b/docs/adr/0079-reduce-twake-mail-mobile-memory-usage.md @@ -0,0 +1,55 @@ +# 0079 - Reduce Twake Mail mobile memory usage + +Date: 2026-04-09 + +## Status + +Accepted + +## Context + +Twake Mail is allocating a lot of memory on mobile. +- ~375 MB on Android (Oneplus 8T) + + + +- ~297 MB on iOS (iPhone 11 Pro) + + + +## Findings + +### `worker_manager` library's inefficient memory allocation (Android & iOS) +- Version `5.0.3` counts the number of processors (x) on the device, create forever-live x - 1 isolates +- Version `7.2.7` adds an ability to create isolate on-demand, and dispose when done. However, it still leaks 1 isolate when init. +### `firebase_messaging` library's eager background Dart isolate (Android) +- FirebaseMessaging.onBackgroundMessage() creates a forever-live isolate even when the app is in foreground. +- Related: https://github.com/firebase/flutterfire/issues/17163 +### Unused `flutter_downloader` library's worker (iOS) +- The only usage was deleted in https://github.com/linagora/tmail-flutter/commit/be8eaf625818b17e60ca65846053cb8c26a71a15#diff-451741ba5146e6ad711c77e4c2fe34958a36595e4926cd43c2ddb97586ef6d88, but the library and initialization process remained, causing 1 forever-live isolate. + +## Decision + +### `worker_manager` +- Upgrade to `7.2.7` +- Create an upstream fix for init's isolate leak +### `firebase_messaging` +- Wait for https://github.com/firebase/flutterfire/pull/18122, update when merged +### `flutter_downloader` +- Remove the library + +## Consequences + +- Android: ~118 MB + + + +- iOS: ~78 MB + + + +- No changes for web + +| Before | After | +| :--- | :--- | +| | | \ No newline at end of file diff --git a/docs/images/android-after-4435.png b/docs/images/android-after-4435.png new file mode 100644 index 0000000000..dfef3fb865 Binary files /dev/null and b/docs/images/android-after-4435.png differ diff --git a/docs/images/android-before-4435.png b/docs/images/android-before-4435.png new file mode 100644 index 0000000000..d5ac290069 Binary files /dev/null and b/docs/images/android-before-4435.png differ diff --git a/docs/images/ios-after-4435.png b/docs/images/ios-after-4435.png new file mode 100644 index 0000000000..b21e0ce82d Binary files /dev/null and b/docs/images/ios-after-4435.png differ diff --git a/docs/images/ios-before-4435.png b/docs/images/ios-before-4435.png new file mode 100644 index 0000000000..bc79dbb512 Binary files /dev/null and b/docs/images/ios-before-4435.png differ diff --git a/docs/images/web-after-4435.png b/docs/images/web-after-4435.png new file mode 100644 index 0000000000..9512058713 Binary files /dev/null and b/docs/images/web-after-4435.png differ diff --git a/docs/images/web-before-4435.png b/docs/images/web-before-4435.png new file mode 100644 index 0000000000..1fbefac788 Binary files /dev/null and b/docs/images/web-before-4435.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b5ca23af67..a9b15dea26 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -97,8 +97,6 @@ PODS: - Flutter - FlutterMacOS - UniversalDetector2 (= 2.0.1) - - flutter_downloader (0.0.1): - - Flutter - flutter_file_dialog (0.0.1): - Flutter - flutter_image_compress_common (1.0.0): @@ -164,7 +162,7 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - lottie-ios (4.4.1) + - lottie-ios (4.4.3) - lottie_native (0.0.1): - Flutter - lottie-ios (~> 4.4.1) @@ -206,10 +204,10 @@ PODS: - ReachabilitySwift (5.2.4) - receive_sharing_intent (1.8.1): - Flutter - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) - - SDWebImageWebPCoder (0.14.6): + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) + - SDWebImageWebPCoder (0.15.0): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - Sentry/HybridSDK (8.56.2) @@ -228,8 +226,6 @@ PODS: - UniversalDetector2 (2.0.1) - url_launcher_ios (0.0.1): - Flutter - - workmanager_apple (0.0.1): - - Flutter DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) @@ -246,7 +242,6 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`) - flutter_charset_detector_darwin (from `.symlinks/plugins/flutter_charset_detector_darwin/darwin`) - - flutter_downloader (from `.symlinks/plugins/flutter_downloader/ios`) - flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`) - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) @@ -272,7 +267,6 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - super_dns_client (from `.symlinks/plugins/super_dns_client/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) SPEC REPOS: trunk: @@ -329,8 +323,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_appauth/ios" flutter_charset_detector_darwin: :path: ".symlinks/plugins/flutter_charset_detector_darwin/darwin" - flutter_downloader: - :path: ".symlinks/plugins/flutter_downloader/ios" flutter_file_dialog: :path: ".symlinks/plugins/flutter_file_dialog/ios" flutter_image_compress_common: @@ -381,8 +373,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/super_dns_client/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - workmanager_apple: - :path: ".symlinks/plugins/workmanager_apple/ios" SPEC CHECKSUMS: app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 @@ -408,7 +398,6 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: d4abcf54856e5d8ba82ed7646ffc83245d4aa448 flutter_charset_detector_darwin: 14f055ebeed6896144cc96b046749df51127a0a3 - flutter_downloader: 78da0da1084e709cbfd3b723c7ea349c71681f09 flutter_file_dialog: ca8d7fbd1772d4f0c2777b4ab20a7787ef4e7dd8 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 @@ -420,7 +409,7 @@ SPEC CHECKSUMS: GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - lottie-ios: e047b1d2e6239b787cc5e9755b988869cf190494 + lottie-ios: fcb5e73e17ba4c983140b7d21095c834b3087418 lottie_native: c2e590a297861fc32a0188cf8dab39aa97f86d81 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 @@ -438,8 +427,8 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 - SDWebImage: f29024626962457f3470184232766516dee8dfea - SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0 share_plus: de6030e33b4e106470e09322d87cf2a4258d2d1d @@ -448,7 +437,6 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 UniversalDetector2: 7c9ffd935cf050eeb19edf7e90f6febe3743a1af url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 PODFILE CHECKSUM: 40b12ce0bc437886ee4f4050970375d7d253708d diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 9f10b9960d..27500c86dd 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,6 +1,5 @@ import UIKit import Flutter -import flutter_downloader import receive_sharing_intent import flutter_local_notifications @@ -34,12 +33,6 @@ import flutter_local_notifications GeneratedPluginRegistrant.register(with: registry) } - FlutterDownloaderPlugin.setPluginRegistrantCallback { registry in - if (!registry.hasPlugin("FlutterDownloaderPlugin")) { - FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!) - } - } - let sharingIntent = SwiftReceiveSharingIntentPlugin.instance if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL { if url.scheme == "mailto" { diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index 516c7005ab..e3692db702 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -4,8 +4,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/account/personal_account.dart'; @@ -68,7 +66,6 @@ class HomeController extends ReloadableController { @override void onInit() { if (PlatformInfo.isMobile) { - _initFlutterDownloader(); _registerReceivingFileSharing(); _registerDeepLinks(); } @@ -96,14 +93,6 @@ class HomeController extends ReloadableController { clearDataAndGoToLoginPage(); } - void _initFlutterDownloader() { - FlutterDownloader - .initialize(debug: kDebugMode) - .then((_) => FlutterDownloader.registerCallback(downloadCallback)); - } - - static void downloadCallback(String id, int status, int progress) {} - Future _handleNavigateToScreen() async { await Future.delayed(2.seconds); final arguments = Get.arguments; diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart index 2ce7efb8fa..4f2f0eaf36 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -172,7 +173,7 @@ class MailboxDataSourceImpl extends MailboxDataSource { StreamController>? onProgressController, }) { return Future.sync(() async { - if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) { return await mailboxAPI.moveFolderContent( session: session, accountId: accountId, diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 7c664c35c4..e7bf21e2dd 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -39,9 +40,8 @@ class MailboxIsolateWorker { final ThreadAPI _threadApi; final EmailAPI _emailApi; - final Executor _isolateExecutor; - MailboxIsolateWorker(this._threadApi, this._emailApi, this._isolateExecutor); + MailboxIsolateWorker(this._threadApi, this._emailApi); Future> markAsMailboxRead( Session session, @@ -50,8 +50,8 @@ class MailboxIsolateWorker { int totalEmailUnread, StreamController> onProgressController ) async { - if (PlatformInfo.isWeb) { - return _handleMarkAsMailboxReadActionOnWeb( + if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) { + return await _handleMarkAsMailboxReadActionOnMainIsolate( session, accountId, mailboxId, @@ -63,108 +63,87 @@ class MailboxIsolateWorker { throw const CanNotGetRootIsolateToken(); } - final result = await _isolateExecutor.execute( - arg1: MailboxMarkAsReadArguments( - session, - _threadApi, - _emailApi, - accountId, - mailboxId, - rootIsolateToken - ), - fun1: _handleMarkAsMailboxReadAction, - notification: (value) { - if (value is List) { - log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${value.length / totalEmailUnread}'); - onProgressController.add(Right(UpdatingMarkAsMailboxReadState( - mailboxId: mailboxId, - totalUnread: totalEmailUnread, - countRead: value.length))); - } - }); - return result; + final args = MailboxMarkAsReadArguments( + session, + _threadApi, + _emailApi, + accountId, + mailboxId, + rootIsolateToken, + ); + return await workerManager.executeWithPort, int>( + _buildMarkAsReadClosure(args), + onMessage: (countRead) { + log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${countRead / totalEmailUnread}'); + onProgressController.add(Right(UpdatingMarkAsMailboxReadState( + mailboxId: mailboxId, + totalUnread: totalEmailUnread, + countRead: countRead))); + }, + ); } } static Future> _handleMarkAsMailboxReadAction( - MailboxMarkAsReadArguments args, - TypeSendPort sendPort + MailboxMarkAsReadArguments args, + SendPort sendPort, ) async { final rootIsolateToken = args.isolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); await HiveCacheConfig.instance.setUp(); - List emailIdsCompleted = List.empty(growable: true); - bool mailboxHasEmails = true; - UTCDate? lastReceivedDate; - EmailId? lastEmailId; - - while (mailboxHasEmails) { - final emailResponse = await args.threadAPI - .getAllEmail( - args.session, - args.accountId, - limit: UnsignedInt(30), - filter: EmailFilterCondition( - inMailbox: args.mailboxId, - notKeyword: KeyWordIdentifier.emailSeen.value, - before: lastReceivedDate), - sort: {}..add( - EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - properties: Properties({ - EmailProperty.id, - EmailProperty.keywords, - EmailProperty.receivedAt, - })) - .then((response) { - var listEmails = response.emailList; - if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { - listEmails = listEmails - .where((email) => email.id != lastEmailId) - .toList(); - } - return EmailsResponse(emailList: listEmails, state: response.state); - }); - final listEmailUnread = emailResponse.emailList; - - log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): listEmailUnread: ${listEmailUnread?.length}'); - - if (listEmailUnread == null || listEmailUnread.isEmpty) { - mailboxHasEmails = false; - } else { - lastEmailId = listEmailUnread.last.id; - lastReceivedDate = listEmailUnread.last.receivedAt; - - final result = await args.emailAPI.markAsRead( - args.session, - args.accountId, - listEmailUnread.listEmailIds, - ReadActions.markAsRead); - - log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.emailIdsSuccess.length}'); - emailIdsCompleted.addAll(result.emailIdsSuccess); - sendPort.send(emailIdsCompleted); - } - } + final emailIdsCompleted = await _executeMarkAsMailboxRead( + threadAPI: args.threadAPI, + emailAPI: args.emailAPI, + session: args.session, + accountId: args.accountId, + mailboxId: args.mailboxId, + onProgress: sendPort.send, + ); log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): TOTAL_READ: ${emailIdsCompleted.length}'); return emailIdsCompleted; } - Future> _handleMarkAsMailboxReadActionOnWeb( + Future> _handleMarkAsMailboxReadActionOnMainIsolate( Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, - StreamController> onProgressController + StreamController> onProgressController, ) async { + final result = await _executeMarkAsMailboxRead( + threadAPI: _threadApi, + emailAPI: _emailApi, + session: session, + accountId: accountId, + mailboxId: mailboxId, + onProgress: (countRead) => onProgressController.add(Right( + UpdatingMarkAsMailboxReadState( + mailboxId: mailboxId, + totalUnread: totalEmailUnread, + countRead: countRead, + ), + )), + ); + log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnMainIsolate(): TOTAL_READ: ${result.length}'); + return result; + } + + static Future> _executeMarkAsMailboxRead({ + required ThreadAPI threadAPI, + required EmailAPI emailAPI, + required Session session, + required AccountId accountId, + required MailboxId mailboxId, + required void Function(int countRead) onProgress, + }) async { List emailIdsCompleted = List.empty(growable: true); bool mailboxHasEmails = true; UTCDate? lastReceivedDate; EmailId? lastEmailId; while (mailboxHasEmails) { - final emailResponse = await _threadApi + final emailResponse = await threadAPI .getAllEmail( session, accountId, @@ -192,7 +171,7 @@ class MailboxIsolateWorker { }); final listEmailUnread = emailResponse.emailList; - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): listEmailUnread: ${listEmailUnread?.length}'); + log('MailboxIsolateWorker::_executeMarkAsMailboxRead(): listEmailUnread: ${listEmailUnread?.length}'); if (listEmailUnread == null || listEmailUnread.isEmpty) { mailboxHasEmails = false; @@ -200,22 +179,19 @@ class MailboxIsolateWorker { lastEmailId = listEmailUnread.last.id; lastReceivedDate = listEmailUnread.last.receivedAt; - final result = await _emailApi.markAsRead( + final result = await emailAPI.markAsRead( session, accountId, listEmailUnread.listEmailIds, ReadActions.markAsRead, ); - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.emailIdsSuccess.length}'); + log('MailboxIsolateWorker::_executeMarkAsMailboxRead(): MARK_READ: ${result.emailIdsSuccess.length}'); emailIdsCompleted.addAll(result.emailIdsSuccess); - onProgressController.add(Right(UpdatingMarkAsMailboxReadState( - mailboxId: mailboxId, - totalUnread: totalEmailUnread, - countRead: emailIdsCompleted.length))); + onProgress(emailIdsCompleted.length); } } - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailIdsCompleted.length}'); + log('MailboxIsolateWorker::_executeMarkAsMailboxRead(): TOTAL_READ: ${emailIdsCompleted.length}'); return emailIdsCompleted; } @@ -230,29 +206,27 @@ class MailboxIsolateWorker { throw const CanNotGetRootIsolateToken(); } - final countEmailsCompleted = await _isolateExecutor.execute( - arg1: MoveFolderContentIsolateArguments( - session: session, - accountId: accountId, - threadAPI: _threadApi, - emailAPI: _emailApi, - currentMailboxId: request.mailboxId, - destinationMailboxId: request.destinationMailboxId, - isolateToken: rootIsolateToken, - markAsRead: request.markAsRead, - ), - fun1: _moveFolderContentIsolateMethod, - notification: (value) { - if (value is int) { - log('$runtimeType::moveFolderContent(): Progress percent is ${value / request.totalEmails}'); - onProgressController?.add( - Right(MoveFolderContentProgressState( - request.mailboxId, - value, - request.totalEmails, - )), - ); - } + final args = MoveFolderContentIsolateArguments( + session: session, + accountId: accountId, + threadAPI: _threadApi, + emailAPI: _emailApi, + currentMailboxId: request.mailboxId, + destinationMailboxId: request.destinationMailboxId, + isolateToken: rootIsolateToken, + markAsRead: request.markAsRead, + ); + final countEmailsCompleted = await workerManager.executeWithPort( + _buildMoveFolderClosure(args), + onMessage: (value) { + log('$runtimeType::moveFolderContent(): Progress percent is ${value / request.totalEmails}'); + onProgressController?.add( + Right(MoveFolderContentProgressState( + request.mailboxId, + value, + request.totalEmails, + )), + ); }, ); @@ -263,9 +237,17 @@ class MailboxIsolateWorker { } } + static Future> Function(SendPort) _buildMarkAsReadClosure( + MailboxMarkAsReadArguments args, + ) => (sendPort) => _handleMarkAsMailboxReadAction(args, sendPort); + + static Future Function(SendPort) _buildMoveFolderClosure( + MoveFolderContentIsolateArguments args, + ) => (sendPort) => _moveFolderContentIsolateMethod(args, sendPort); + static Future _moveFolderContentIsolateMethod( MoveFolderContentIsolateArguments args, - TypeSendPort sendPort, + SendPort sendPort, ) async { final rootIsolateToken = args.isolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 2505ddfc84..1352921765 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -30,9 +31,8 @@ import 'package:worker_manager/worker_manager.dart'; class ThreadIsolateWorker { final ThreadAPI _threadAPI; final EmailAPI _emailAPI; - final Executor _isolateExecutor; - ThreadIsolateWorker(this._threadAPI, this._emailAPI, this._isolateExecutor); + ThreadIsolateWorker(this._threadAPI, this._emailAPI); Future> emptyMailboxFolder( Session session, @@ -41,31 +41,29 @@ class ThreadIsolateWorker { int totalEmails, StreamController> onProgressController ) async { - if (PlatformInfo.isWeb) { - return _emptyMailboxFolderOnWeb(session, accountId, mailboxId, totalEmails, onProgressController); + if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) { + return _emptyMailboxFolderOnMainIsolate(session, accountId, mailboxId, totalEmails, onProgressController); } else { final rootIsolateToken = RootIsolateToken.instance; if (rootIsolateToken == null) { throw const CanNotGetRootIsolateToken(); } - final result = await _isolateExecutor.execute( - arg1: EmptyMailboxFolderArguments( - session, - _threadAPI, - _emailAPI, - accountId, - mailboxId, - rootIsolateToken - ), - fun1: _emptyMailboxFolderAction, - notification: (value) { - if (value is List) { - log('ThreadIsolateWorker::emptyMailboxFolder(): processed ${value.length} - totalEmails $totalEmails'); - onProgressController.add(Right(EmptyingFolderState( - mailboxId, value.length, totalEmails - ))); - } + final args = EmptyMailboxFolderArguments( + session, + _threadAPI, + _emailAPI, + accountId, + mailboxId, + rootIsolateToken, + ); + final result = await workerManager.executeWithPort, int>( + _buildEmptyMailboxClosure(args), + onMessage: (processedCount) { + log('ThreadIsolateWorker::emptyMailboxFolder(): processed $processedCount - totalEmails $totalEmails'); + onProgressController.add(Right(EmptyingFolderState( + mailboxId, processedCount, totalEmails + ))); }, ); @@ -77,9 +75,13 @@ class ThreadIsolateWorker { } } + static Future> Function(SendPort) _buildEmptyMailboxClosure( + EmptyMailboxFolderArguments args, + ) => (sendPort) => _emptyMailboxFolderAction(args, sendPort); + static Future> _emptyMailboxFolderAction( EmptyMailboxFolderArguments args, - TypeSendPort sendPort + SendPort sendPort, ) async { final rootIsolateToken = args.isolateToken; BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); @@ -119,7 +121,7 @@ class ThreadIsolateWorker { args.accountId, newEmailList.listEmailIds); emailListCompleted.addAll(listEmailIdDeleted.emailIdsSuccess); - sendPort.send(emailListCompleted); + sendPort.send(emailListCompleted.length); } else { hasEmails = false; } @@ -128,7 +130,7 @@ class ThreadIsolateWorker { return emailListCompleted; } - Future> _emptyMailboxFolderOnWeb( + Future> _emptyMailboxFolderOnMainIsolate( Session session, AccountId accountId, MailboxId mailboxId, @@ -158,7 +160,7 @@ class ThreadIsolateWorker { newEmailList = newEmailList.where((email) => email.id != lastEmail!.id).toList(); } - log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ${newEmailList.length}'); + log('ThreadIsolateWorker::_emptyMailboxFolderOnMainIsolate(): ${newEmailList.length}'); if (newEmailList.isNotEmpty) { lastEmail = newEmailList.last; @@ -176,7 +178,7 @@ class ThreadIsolateWorker { hasEmails = false; } } - log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): TOTAL_REMOVE: ${emailListCompleted.length}'); + log('ThreadIsolateWorker::_emptyMailboxFolderOnMainIsolate(): TOTAL_REMOVE: ${emailListCompleted.length}'); return emailListCompleted; } } diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart index aab9b5fca9..64fb7f736a 100644 --- a/lib/features/upload/data/network/file_uploader.dart +++ b/lib/features/upload/data/network/file_uploader.dart @@ -31,12 +31,10 @@ class FileUploader { static const String filePathExtraKey = 'path'; final DioClient _dioClient; - final worker.Executor _isolateExecutor; final FileUtils _fileUtils; FileUploader( this._dioClient, - this._isolateExecutor, this._fileUtils, ); @@ -49,8 +47,8 @@ class FileUploader { StreamController>? onSendController, } ) async { - if (PlatformInfo.isWeb) { - return _handleUploadAttachmentActionOnWeb( + if (PlatformInfo.isWeb || Platform.numberOfProcessors == 1) { + return _handleUploadAttachmentActionOnMainIsolate( uploadId, fileInfo, uploadUri, @@ -63,31 +61,33 @@ class FileUploader { throw const CanNotGetRootIsolateToken(); } - return await _isolateExecutor.execute( - arg1: UploadFileArguments( - _dioClient, - _fileUtils, - uploadId, - fileInfo, - uploadUri, - rootIsolateToken, - ), - fun1: _handleUploadAttachmentAction, - notification: (value) { - if (value is Success) { - log('FileUploader::uploadAttachment(): onUpdateProgress: $value'); - onSendController?.add(Right(value)); - } - } + final args = UploadFileArguments( + _dioClient, + _fileUtils, + uploadId, + fileInfo, + uploadUri, + rootIsolateToken, + ); + return await worker.workerManager.executeWithPort( + _buildUploadClosure(args), + onMessage: (value) { + log('FileUploader::uploadAttachment(): onUpdateProgress: $value'); + onSendController?.add(Right(value)); + }, ) .then((value) => value) .catchError((error) => throw error); } } + static Future Function(worker.SendPort) _buildUploadClosure( + UploadFileArguments args, + ) => (sendPort) => _handleUploadAttachmentAction(args, sendPort); + static Future _handleUploadAttachmentAction( - UploadFileArguments argsUpload, - worker.TypeSendPort sendPort + UploadFileArguments argsUpload, + worker.SendPort sendPort, ) async { try { final rootIsolateToken = argsUpload.isolateToken; @@ -159,7 +159,7 @@ class FileUploader { } } - Future _handleUploadAttachmentActionOnWeb( + Future _handleUploadAttachmentActionOnMainIsolate( UploadTaskId uploadId, FileInfo fileInfo, Uri uploadUri, @@ -188,7 +188,7 @@ class FileUploader { data: BodyBytesStream.fromBytes(fileInfo.bytes!), cancelToken: cancelToken, onSendProgress: (count, total) { - log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); + log('FileUploader::_handleUploadAttachmentActionOnMainIsolate():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); onSendController?.add( Right(UploadingAttachmentUploadState( uploadId, @@ -198,7 +198,7 @@ class FileUploader { ); } ); - log('FileUploader::_handleUploadAttachmentActionOnWeb(): RESULT_JSON = $resultJson'); + log('FileUploader::_handleUploadAttachmentActionOnMainIsolate(): RESULT_JSON = $resultJson'); if (fileInfo.mimeType == FileUtils.TEXT_PLAIN_MIME_TYPE) { final fileCharset = await _fileUtils.getCharsetFromBytes(fileInfo.bytes!); return _parsingResponse( diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 2567e469e3..dd72b59453 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -40,7 +40,6 @@ import 'package:tmail_ui_user/main/exceptions/thrower/remote_exception_thrower.d import 'package:tmail_ui_user/main/exceptions/thrower/send_email_exception_thrower.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:uuid/uuid.dart'; -import 'package:worker_manager/worker_manager.dart'; class NetworkBindings extends Bindings { @@ -85,7 +84,6 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), )); - Get.put(Executor()); } void _bindingInterceptors() { diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index 0ec431d37d..8acde6ce4f 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -24,7 +24,6 @@ import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:uuid/uuid.dart'; -import 'package:worker_manager/worker_manager.dart'; class NetworkIsolateBindings extends Bindings { @@ -96,16 +95,13 @@ class NetworkIsolateBindings extends Bindings { Get.put(ThreadIsolateWorker( Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null), Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null), - Get.find(), )); Get.put(MailboxIsolateWorker( Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null), Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null), - Get.find(), )); Get.put(FileUploader( Get.find(tag: PlatformInfo.isMobile ? BindingTag.isolateTag : null), - Get.find(), Get.find(), )); } diff --git a/lib/main/main_entry.dart b/lib/main/main_entry.dart index c251152f44..c2c0aad95c 100644 --- a/lib/main/main_entry.dart +++ b/lib/main/main_entry.dart @@ -3,7 +3,6 @@ import 'package:core/utils/build_utils.dart'; import 'package:core/utils/config/env_loader.dart'; import 'package:core/utils/platform_info.dart'; import 'package:flutter/widgets.dart'; -import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/main.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; @@ -27,7 +26,10 @@ Future runTmailPreload() async { if (PlatformInfo.isWeb) AssetPreloader.preloadHtmlEditorAssets(), ], eagerError: false); - await Get.find().warmUp(log: BuildUtils.isDebugMode); + if (PlatformInfo.isMobile) { + await workerManager.init(dynamicSpawning: true); + workerManager.log = BuildUtils.isDebugMode; + } await CozyIntegration.integrateCozy(); await HiveCacheConfig.instance.initializeEncryptionKey(); diff --git a/pubspec.lock b/pubspec.lock index 190129348d..fff891976b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -773,14 +773,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" - flutter_downloader: - dependency: "direct main" - description: - name: flutter_downloader - sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118" - url: "https://pub.dev" - source: hosted - version: "1.12.0" flutter_file_dialog: dependency: "direct main" description: @@ -2583,43 +2575,12 @@ packages: worker_manager: dependency: "direct main" description: - name: worker_manager - sha256: "42501e49ee0acad9eeda562984e3dcfe6fe3d26f2d8dc410bd76308a86447eb5" - url: "https://pub.dev" - source: hosted - version: "5.0.3" - workmanager: - dependency: "direct main" - description: - name: workmanager - sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10" - url: "https://pub.dev" - source: hosted - version: "0.9.0+3" - workmanager_android: - dependency: transitive - description: - name: workmanager_android - sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7" - url: "https://pub.dev" - source: hosted - version: "0.9.0+2" - workmanager_apple: - dependency: transitive - description: - name: workmanager_apple - sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca" - url: "https://pub.dev" - source: hosted - version: "0.9.1+2" - workmanager_platform_interface: - dependency: transitive - description: - name: workmanager_platform_interface - sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 - url: "https://pub.dev" - source: hosted - version: "0.9.1+1" + path: "." + ref: "hotfix/worker-init-memory-leak" + resolved-ref: dd04544217c9fcc08b2a32634583f38d22cc2309 + url: "https://github.com/linagora/worker_manager.git" + source: git + version: "7.2.7" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d7a68d139..a08f71c575 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -143,8 +143,6 @@ dependencies: uuid: 3.0.7 - flutter_downloader: 1.12.0 - external_path: 2.2.0 path_provider: 2.1.5 @@ -177,7 +175,11 @@ dependencies: percent_indicator: 4.2.2 - worker_manager: 5.0.3 + # TODO: Replace with upstream when https://github.com/dsrenesanse/worker_manager/pull/123 is merged + worker_manager: + git: + url: https://github.com/linagora/worker_manager.git + ref: hotfix/worker-init-memory-leak async: 2.13.0 @@ -211,8 +213,6 @@ dependencies: intl: 0.20.2 - workmanager: 0.9.0+3 - flutter_typeahead: 5.0.2 flutter_keyboard_visibility: 6.0.0 diff --git a/test/features/identity_creator/presentation/identity_creator_controller_test.dart b/test/features/identity_creator/presentation/identity_creator_controller_test.dart index 36dc0ecaa3..6acf57c452 100644 --- a/test/features/identity_creator/presentation/identity_creator_controller_test.dart +++ b/test/features/identity_creator/presentation/identity_creator_controller_test.dart @@ -52,7 +52,6 @@ import 'package:tmail_ui_user/main/universal_import/html_stub.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:tmail_ui_user/main/utils/twake_app_manager.dart'; import 'package:uuid/uuid.dart'; -import 'package:worker_manager/worker_manager.dart'; import 'identity_creator_controller_test.mocks.dart'; @@ -76,7 +75,6 @@ import 'identity_creator_controller_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), - MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -159,7 +157,6 @@ void main() { mockSaveIdentityCacheOnWebInteractor = MockSaveIdentityCacheOnWebInteractor(); Get.put(MockDioClient(), tag: BindingTag.isolateTag); - Get.put(MockExecutor()); Get.put(MockFileUtils()); Get.put(MockFileUploader()); Get.put(MockRemoteExceptionThrower());