import 'dart:async'; import 'dart:isolate'; import "package:dart_ui_isolate/dart_ui_isolate.dart"; import "package:flutter/foundation.dart" show kDebugMode; import "package:logging/logging.dart"; import "package:photos/core/error-reporting/isolate_logging.dart"; import "package:photos/models/base/id.dart"; import "package:photos/services/isolate_functions.dart"; import "package:synchronized/synchronized.dart"; abstract class SuperIsolate { Logger get logger; Timer? _inactivityTimer; final Duration _inactivityDuration = const Duration(seconds: 120); int _activeTasks = 0; final _initIsolateLock = Lock(); final _functionLock = Lock(); bool get isDartUiIsolate; bool get shouldAutomaticDispose; String get isolateName; late dynamic _isolate; late ReceivePort _receivePort; late SendPort _mainSendPort; bool get isIsolateSpawned => _isIsolateSpawned; bool _isIsolateSpawned = false; Future _initIsolate() async { return _initIsolateLock.synchronized(() async { if (_isIsolateSpawned) return; _receivePort = ReceivePort(); try { _isolate = isDartUiIsolate ? await DartUiIsolate.spawn( _isolateMain, _receivePort.sendPort, ) : await Isolate.spawn( _isolateMain, _receivePort.sendPort, debugName: isolateName, ); _mainSendPort = await _receivePort.first as SendPort; if (shouldAutomaticDispose) _resetInactivityTimer(); logger.info('initIsolate done'); _isIsolateSpawned = true; } catch (e) { logger.severe('Could not spawn isolate', e); _isIsolateSpawned = false; } }); } @pragma('vm:entry-point') static void _isolateMain(SendPort mainSendPort) async { Logger.root.level = kDebugMode ? Level.ALL : Level.INFO; final IsolateLogger isolateLogger = IsolateLogger(); Logger.root.onRecord.listen(isolateLogger.onLogRecordInIsolate); final receivePort = ReceivePort(); mainSendPort.send(receivePort.sendPort); receivePort.listen((message) async { final taskID = message[0] as String; final functionIndex = message[1] as int; final function = IsolateOperation.values[functionIndex]; final args = message[2] as Map; final sendPort = message[3] as SendPort; late final Object data; try { data = await isolateFunction(function, args); } catch (e, stackTrace) { data = { 'error': e.toString(), 'stackTrace': stackTrace.toString(), }; } final logs = List.from(isolateLogger.getLogStringsAndClear()); sendPort.send({"taskID": taskID, "data": data, "logs": logs}); }); } /// The common method to run any operation in the isolate. /// It sends the [message] to [_isolateMain] and waits for the result. /// The actual function executed is [isolateFunction]. Future runInIsolate( IsolateOperation operation, Map args, ) async { await _initIsolate(); return _functionLock.synchronized(() async { if (shouldAutomaticDispose) _resetInactivityTimer(); if (postFunctionlockStop(operation)) { return null; } final completer = Completer(); final answerPort = ReceivePort(); _activeTasks++; final taskID = newIsolateTaskID(operation.name); _mainSendPort.send([taskID, operation.index, args, answerPort.sendPort]); answerPort.listen((receivedMessage) { if (receivedMessage['taskID'] != taskID) { logger.severe("Received isolate message with wrong taskID"); return; } final logs = receivedMessage['logs'] as List; IsolateLogger.handLogStringsToMainLogger(logs); final data = receivedMessage['data']; if (data is Map && data.containsKey('error')) { // Handle the error final errorMessage = data['error']; final errorStackTrace = data['stackTrace']; final exception = Exception(errorMessage); final stackTrace = StackTrace.fromString(errorStackTrace); completer.completeError(exception, stackTrace); } else { completer.complete(data); } }); _activeTasks--; return completer.future; }); } bool postFunctionlockStop(IsolateOperation operation) => false; /// Resets a timer that kills the isolate after a certain amount of inactivity. /// /// Should be called after initialization (e.g. inside `init()`) and after every call to isolate (e.g. inside `_runInIsolate()`) void _resetInactivityTimer() { _inactivityTimer?.cancel(); _inactivityTimer = Timer(_inactivityDuration, () { if (_activeTasks > 0) { logger.info('Tasks are still running. Delaying isolate disposal.'); // Optionally, reschedule the timer to check again later. _resetInactivityTimer(); } else { logger.info( 'Isolate has been inactive for ${_inactivityDuration.inSeconds} seconds with no tasks running. Killing isolate.', ); _disposeIsolate(); } }); } Future onDispose() async {} void _disposeIsolate() async { if (!_isIsolateSpawned) return; logger.info('Disposing isolate'); await onDispose(); _isIsolateSpawned = false; _isolate.kill(); _receivePort.close(); _inactivityTimer?.cancel(); } }