From 53fff399a17936e8b9c17846c199bdf753e6464e Mon Sep 17 00:00:00 2001 From: Bennik2000 Date: Tue, 8 Sep 2020 08:52:04 +0200 Subject: [PATCH 1/3] Move rapla result parsing into separate isolate --- lib/common/appstart/service_injector.dart | 5 +- .../isolate_schedule_source_decorator.dart | 100 ++++++++++++++++++ .../weekly_schedule_view_model.dart | 19 ++-- .../weeklyschedule/weekly_schedule_page.dart | 100 +++++++++--------- 4 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 lib/schedule/service/isolate_schedule_source_decorator.dart diff --git a/lib/common/appstart/service_injector.dart b/lib/common/appstart/service_injector.dart index eb796fe4..e8b2206a 100644 --- a/lib/common/appstart/service_injector.dart +++ b/lib/common/appstart/service_injector.dart @@ -12,6 +12,7 @@ import 'package:dhbwstudentapp/schedule/business/schedule_source_setup.dart'; import 'package:dhbwstudentapp/schedule/data/schedule_entry_repository.dart'; import 'package:dhbwstudentapp/schedule/data/schedule_query_information_repository.dart'; import 'package:dhbwstudentapp/schedule/service/error_report_schedule_source_decorator.dart'; +import 'package:dhbwstudentapp/schedule/service/isolate_schedule_source_decorator.dart'; import 'package:dhbwstudentapp/schedule/service/rapla/rapla_schedule_source.dart'; import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; import 'package:kiwi/kiwi.dart'; @@ -31,7 +32,9 @@ void injectServices() { SecureStorageAccess(), )); c.registerInstance( - ErrorReportScheduleSourceDecorator(RaplaScheduleSource()), + IsolateScheduleSourceDecorator( + ErrorReportScheduleSourceDecorator(RaplaScheduleSource()), + ), ); c.registerInstance(DatabaseAccess()); c.registerInstance(ScheduleEntryRepository( diff --git a/lib/schedule/service/isolate_schedule_source_decorator.dart b/lib/schedule/service/isolate_schedule_source_decorator.dart new file mode 100644 index 00000000..9035ced8 --- /dev/null +++ b/lib/schedule/service/isolate_schedule_source_decorator.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:isolate'; + +import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/schedule/model/schedule.dart'; +import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; + +scheduleSourceIsolateEntryPoint(SendPort sendPort) async { + var port = ReceivePort(); + sendPort.send(port.sendPort); + + CancellationToken token; + + await for (var message in port) { + if (message["type"] == "execute") { + token = CancellationToken(); + executeQuerySchedule(message, sendPort, token); + } else if (message["type"] == "cancel") { + token?.cancel(); + } + } +} + +Future executeQuerySchedule( + Map map, + SendPort sendPort, + CancellationToken token, +) async { + try { + ScheduleSource source = map["source"]; + DateTime from = map["from"]; + DateTime to = map["to"]; + + var result = await source.querySchedule(from, to, token); + + sendPort.send(result); + } on OperationCancelledException catch (_) { + sendPort.send(null); + } +} + +class IsolateScheduleSourceDecorator extends ScheduleSource { + final ScheduleSource _scheduleSource; + + Stream _isolateToMain; + Isolate _isolate; + SendPort _sendPort; + + IsolateScheduleSourceDecorator(this._scheduleSource); + + @override + Future querySchedule(DateTime from, DateTime to, + [CancellationToken cancellationToken]) async { + await _initializeIsolate(); + + cancellationToken.setCancellationCallback(() { + _sendPort.send({"type": "cancel"}); + }); + + _sendPort.send({ + "type": "execute", + "source": _scheduleSource, + "from": from, + "to": to, + }); + + final completer = Completer(); + final subscription = _isolateToMain.listen((result) { + cancellationToken.setCancellationCallback(null); + completer.complete(result); + }); + + final result = await completer.future; + subscription.cancel(); + + return result; + } + + Future _initializeIsolate() async { + if (_isolate != null && _isolateToMain != null && _sendPort != null) return; + + var isolateToMain = ReceivePort(); + + // Use a broadcast stream. The normal RecievePort closes after one subscription + _isolateToMain = isolateToMain.asBroadcastStream(); + _isolate = await Isolate.spawn( + scheduleSourceIsolateEntryPoint, isolateToMain.sendPort); + _sendPort = await _isolateToMain.first; + } + + @override + void setEndpointUrl(String url) { + _scheduleSource.setEndpointUrl(url); + } + + @override + void validateEndpointUrl(String url) { + _scheduleSource.validateEndpointUrl(url); + } +} diff --git a/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart b/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart index 24682a92..74b69f00 100644 --- a/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart +++ b/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:math'; import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; +import 'package:dhbwstudentapp/common/util/cancelable_mutex.dart'; import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:dhbwstudentapp/common/util/date_utils.dart'; import 'package:dhbwstudentapp/schedule/business/schedule_provider.dart'; import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; -import 'package:mutex/mutex.dart'; class WeeklyScheduleViewModel extends BaseViewModel { static const Duration weekDuration = Duration(days: 7); @@ -35,8 +35,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { Timer _errorResetTimer; Timer _updateNowTimer; - CancellationToken _updateScheduleCancellationToken; - Mutex _mutex = Mutex(); + final CancelableMutex _updateMutex = CancelableMutex(); DateTime lastRequestedStart; DateTime lastRequestedEnd; @@ -102,19 +101,13 @@ class WeeklyScheduleViewModel extends BaseViewModel { lastRequestedEnd = end; lastRequestedStart = start; - _updateScheduleCancellationToken?.cancel(); - - await _mutex.acquire(); - - _updateScheduleCancellationToken = null; + await _updateMutex.acquireAndCancelOther(); if (lastRequestedStart != start || lastRequestedEnd != end) { - _mutex.release(); + _updateMutex.release(); return; } - _updateScheduleCancellationToken = new CancellationToken(); - try { isUpdating = true; notifyListeners("isUpdating"); @@ -122,7 +115,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { await _doUpdateSchedule(start, end); } catch (_) {} finally { isUpdating = false; - _mutex.release(); + _updateMutex.release(); notifyListeners("isUpdating"); } } @@ -130,7 +123,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { Future _doUpdateSchedule(DateTime start, DateTime end) async { print("Refreshing schedule..."); - var cancellationToken = _updateScheduleCancellationToken; + var cancellationToken = _updateMutex.token; var cachedSchedule = await scheduleProvider.getCachedSchedule(start, end); cancellationToken.throwIfCancelled(); diff --git a/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart b/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart index be5674fd..e9712bbf 100644 --- a/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart +++ b/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart @@ -74,23 +74,7 @@ class _WeeklySchedulePageState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FlatButton( - child: Icon(Icons.chevron_left), - onPressed: _previousWeek, - ), - FlatButton( - child: Icon(Icons.today), - onPressed: _goToToday, - ), - FlatButton( - child: Icon(Icons.chevron_right), - onPressed: _nextWeek, - ), - ], - ), + _buildNavigationButtonBar(), Expanded( child: Stack( children: [ @@ -99,38 +83,38 @@ class _WeeklySchedulePageState extends State { child: PropertyChangeConsumer( properties: ["weekSchedule", "now"], builder: (BuildContext context, - WeeklyScheduleViewModel model, - Set properties) => - PageTransitionSwitcher( - reverse: !model.didUpdateScheduleIntoFuture, - duration: Duration(milliseconds: 300), - transitionBuilder: (Widget child, - Animation animation, - Animation secondaryAnimation) => - SharedAxisTransition( - child: child, - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: - SharedAxisTransitionType.horizontal, - ), - child: ScheduleWidget( - key: ValueKey( - model.currentDateStart.toIso8601String(), + WeeklyScheduleViewModel model, Set properties) { + return PageTransitionSwitcher( + reverse: !model.didUpdateScheduleIntoFuture, + duration: Duration(milliseconds: 300), + transitionBuilder: (Widget child, + Animation animation, + Animation secondaryAnimation) => + SharedAxisTransition( + child: child, + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: + SharedAxisTransitionType.horizontal, + ), + child: ScheduleWidget( + key: ValueKey( + model.currentDateStart.toIso8601String(), + ), + schedule: model.weekSchedule, + displayStart: model.clippedDateStart ?? + model.currentDateStart, + displayEnd: model.clippedDateEnd ?? + model.currentDateEnd, + onScheduleEntryTap: (entry) { + _onScheduleEntryTap(context, entry); + }, + now: model.now, + displayEndHour: model.displayEndHour, + displayStartHour: model.displayStartHour, ), - schedule: model.weekSchedule, - displayStart: model.clippedDateStart ?? - model.currentDateStart, - displayEnd: - model.clippedDateEnd ?? model.currentDateEnd, - onScheduleEntryTap: (entry) { - _onScheduleEntryTap(context, entry); - }, - now: model.now, - displayEndHour: model.displayEndHour, - displayStartHour: model.displayStartHour, - ), - ), + ); + }, ), ), PropertyChangeConsumer( @@ -154,6 +138,26 @@ class _WeeklySchedulePageState extends State { ); } + Row _buildNavigationButtonBar() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FlatButton( + child: Icon(Icons.chevron_left), + onPressed: _previousWeek, + ), + FlatButton( + child: Icon(Icons.today), + onPressed: _goToToday, + ), + FlatButton( + child: Icon(Icons.chevron_right), + onPressed: _nextWeek, + ), + ], + ); + } + Widget buildErrorDisplay(BuildContext context) { return PropertyChangeConsumer( properties: [ From 784e33dbfb146db5b9971060755bbf698bba4935 Mon Sep 17 00:00:00 2001 From: Bennik2000 Date: Tue, 8 Sep 2020 16:40:00 +0200 Subject: [PATCH 2/3] Optimize isolate schedule source for background tasks --- lib/common/appstart/app_initializer.dart | 2 +- lib/common/appstart/service_injector.dart | 10 ++++++---- .../service/isolate_schedule_source_decorator.dart | 11 +++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/common/appstart/app_initializer.dart b/lib/common/appstart/app_initializer.dart index 1358a5df..826b573b 100644 --- a/lib/common/appstart/app_initializer.dart +++ b/lib/common/appstart/app_initializer.dart @@ -25,7 +25,7 @@ Future initializeApp(bool isBackground) async { WidgetsFlutterBinding.ensureInitialized(); - injectServices(); + injectServices(isBackground); if (isBackground) { await LocalizationInitialize.fromPreferences(KiwiContainer().resolve()) diff --git a/lib/common/appstart/service_injector.dart b/lib/common/appstart/service_injector.dart index e8b2206a..e509a587 100644 --- a/lib/common/appstart/service_injector.dart +++ b/lib/common/appstart/service_injector.dart @@ -23,7 +23,7 @@ bool _isInjected = false; /// This function injects all instances into the KiwiContainer. You can get a /// singleton instance of a registered type using KiwiContainer().resolve() /// -void injectServices() { +void injectServices(bool isBackground) { if (_isInjected) return; KiwiContainer c = KiwiContainer(); @@ -32,9 +32,11 @@ void injectServices() { SecureStorageAccess(), )); c.registerInstance( - IsolateScheduleSourceDecorator( - ErrorReportScheduleSourceDecorator(RaplaScheduleSource()), - ), + isBackground + ? ErrorReportScheduleSourceDecorator(RaplaScheduleSource()) + : IsolateScheduleSourceDecorator( + ErrorReportScheduleSourceDecorator(RaplaScheduleSource()), + ), ); c.registerInstance(DatabaseAccess()); c.registerInstance(ScheduleEntryRepository( diff --git a/lib/schedule/service/isolate_schedule_source_decorator.dart b/lib/schedule/service/isolate_schedule_source_decorator.dart index 9035ced8..6e82ffd7 100644 --- a/lib/schedule/service/isolate_schedule_source_decorator.dart +++ b/lib/schedule/service/isolate_schedule_source_decorator.dart @@ -5,7 +5,8 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; -scheduleSourceIsolateEntryPoint(SendPort sendPort) async { +void scheduleSourceIsolateEntryPoint(SendPort sendPort) async { + // Using the given send port, send back a send port for two way communication var port = ReceivePort(); sendPort.send(port.sendPort); @@ -14,14 +15,14 @@ scheduleSourceIsolateEntryPoint(SendPort sendPort) async { await for (var message in port) { if (message["type"] == "execute") { token = CancellationToken(); - executeQuerySchedule(message, sendPort, token); + executeQueryScheduleMessage(message, sendPort, token); } else if (message["type"] == "cancel") { token?.cancel(); } } } -Future executeQuerySchedule( +Future executeQueryScheduleMessage( Map map, SendPort sendPort, CancellationToken token, @@ -53,6 +54,8 @@ class IsolateScheduleSourceDecorator extends ScheduleSource { [CancellationToken cancellationToken]) async { await _initializeIsolate(); + // Use the cancellation token to send a cancel message. + // The isolate then uses a new instance to cancel the request cancellationToken.setCancellationCallback(() { _sendPort.send({"type": "cancel"}); }); @@ -81,7 +84,7 @@ class IsolateScheduleSourceDecorator extends ScheduleSource { var isolateToMain = ReceivePort(); - // Use a broadcast stream. The normal RecievePort closes after one subscription + // Use a broadcast stream. The normal ReceivePort closes after one subscription _isolateToMain = isolateToMain.asBroadcastStream(); _isolate = await Isolate.spawn( scheduleSourceIsolateEntryPoint, isolateToMain.sendPort); From 32ca6832009ff50ab1e59f03bcde02e1a0cf1eef Mon Sep 17 00:00:00 2001 From: Bennik2000 Date: Tue, 8 Sep 2020 17:05:33 +0200 Subject: [PATCH 3/3] Minor code cleanup --- .../isolate_schedule_source_decorator.dart | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/lib/schedule/service/isolate_schedule_source_decorator.dart b/lib/schedule/service/isolate_schedule_source_decorator.dart index 6e82ffd7..cc8aed43 100644 --- a/lib/schedule/service/isolate_schedule_source_decorator.dart +++ b/lib/schedule/service/isolate_schedule_source_decorator.dart @@ -5,41 +5,9 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; -void scheduleSourceIsolateEntryPoint(SendPort sendPort) async { - // Using the given send port, send back a send port for two way communication - var port = ReceivePort(); - sendPort.send(port.sendPort); - - CancellationToken token; - - await for (var message in port) { - if (message["type"] == "execute") { - token = CancellationToken(); - executeQueryScheduleMessage(message, sendPort, token); - } else if (message["type"] == "cancel") { - token?.cancel(); - } - } -} - -Future executeQueryScheduleMessage( - Map map, - SendPort sendPort, - CancellationToken token, -) async { - try { - ScheduleSource source = map["source"]; - DateTime from = map["from"]; - DateTime to = map["to"]; - - var result = await source.querySchedule(from, to, token); - - sendPort.send(result); - } on OperationCancelledException catch (_) { - sendPort.send(null); - } -} - +/// +/// ScheduleSource decorator which executes the [querySchedule] in a separated isolate +/// class IsolateScheduleSourceDecorator extends ScheduleSource { final ScheduleSource _scheduleSource; @@ -101,3 +69,38 @@ class IsolateScheduleSourceDecorator extends ScheduleSource { _scheduleSource.validateEndpointUrl(url); } } + +void scheduleSourceIsolateEntryPoint(SendPort sendPort) async { + // Using the given send port, send back a send port for two way communication + var port = ReceivePort(); + sendPort.send(port.sendPort); + + CancellationToken token; + + await for (var message in port) { + if (message["type"] == "execute") { + token = CancellationToken(); + executeQueryScheduleMessage(message, sendPort, token); + } else if (message["type"] == "cancel") { + token?.cancel(); + } + } +} + +Future executeQueryScheduleMessage( + Map map, + SendPort sendPort, + CancellationToken token, +) async { + try { + ScheduleSource source = map["source"]; + DateTime from = map["from"]; + DateTime to = map["to"]; + + var result = await source.querySchedule(from, to, token); + + sendPort.send(result); + } on OperationCancelledException catch (_) { + sendPort.send(null); + } +}