ente/mobile/lib/services/isolate_service.dart
2024-09-02 18:36:31 +02:00

171 lines
5.4 KiB
Dart

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<void> _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<String, dynamic>;
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<String>.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<dynamic> runInIsolate(
IsolateOperation operation,
Map<String, dynamic> args,
) async {
await _initIsolate();
return _functionLock.synchronized(() async {
if (shouldAutomaticDispose) _resetInactivityTimer();
if (postFunctionlockStop(operation)) {
return null;
}
final completer = Completer<dynamic>();
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<String>;
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<void> onDispose() async {}
void _disposeIsolate() async {
if (!_isIsolateSpawned) return;
logger.info('Disposing isolate');
await onDispose();
_isIsolateSpawned = false;
_isolate.kill();
_receivePort.close();
_inactivityTimer?.cancel();
}
}