From e02593def39927ebab667086bd7c42bed1aa781c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 2 Feb 2021 19:54:28 +0900 Subject: [PATCH] perf: improved task pause/resume --- lib/image_providers/thumbnail_provider.dart | 16 ++-- lib/services/image_file_service.dart | 1 - lib/services/service_policy.dart | 96 ++++++++++----------- lib/widgets/debug/overlay.dart | 3 +- 4 files changed, 54 insertions(+), 62 deletions(-) diff --git a/lib/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart index fac117ab4..0f7e9d063 100644 --- a/lib/image_providers/thumbnail_provider.dart +++ b/lib/image_providers/thumbnail_provider.dart @@ -21,7 +21,7 @@ class ThumbnailProvider extends ImageProvider { ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), - scale: key.scale, + scale: 1.0, informationCollector: () sync* { yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}'); }, @@ -69,7 +69,7 @@ class ThumbnailProviderKey { final int pageId, rotationDegrees; final bool isFlipped; final int dateModifiedSecs; - final double extent, scale; + final double extent; const ThumbnailProviderKey({ @required this.uri, @@ -79,33 +79,27 @@ class ThumbnailProviderKey { @required this.isFlipped, @required this.dateModifiedSecs, this.extent = 0, - this.scale = 1, }) : assert(uri != null), assert(mimeType != null), assert(rotationDegrees != null), assert(isFlipped != null), assert(dateModifiedSecs != null), - assert(extent != null), - assert(scale != null); + assert(extent != null); @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.pageId == pageId && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale; + return other is ThumbnailProviderKey && other.uri == uri && other.pageId == pageId && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent; } @override int get hashCode => hashValues( uri, - mimeType, pageId, - rotationDegrees, - isFlipped, dateModifiedSecs, extent, - scale, ); @override - String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}'; + String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, pageId=$pageId, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent}'; } diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 723d1312e..75ad7eda1 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -204,7 +204,6 @@ class ImageFileService { } return null; }, -// debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}', priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, ); diff --git a/lib/services/service_policy.dart b/lib/services/service_policy.dart index c4f8fa043..ab16a69eb 100644 --- a/lib/services/service_policy.dart +++ b/lib/services/service_policy.dart @@ -9,8 +9,8 @@ final ServicePolicy servicePolicy = ServicePolicy._private(); class ServicePolicy { final StreamController _queueStreamController = StreamController.broadcast(); final Map> _paused = {}; - final SplayTreeMap> _queues = SplayTreeMap(); - final Queue<_Task> _runningQueue = Queue(); + final SplayTreeMap> _queues = SplayTreeMap(); + final LinkedHashMap _runningQueue = LinkedHashMap(); // magic number static const concurrentTaskMax = 4; @@ -22,57 +22,59 @@ class ServicePolicy { Future call( Future Function() platformCall, { int priority = ServiceCallPriority.normal, - String debugLabel, Object key, }) { + Completer completer; _Task task; key ??= platformCall.hashCode; - final priorityTask = _paused.remove(key); - if (priorityTask != null) { - debugPrint('resume task with key=$key'); - priority = priorityTask.item1; - task = priorityTask.item2; + final toResume = _paused.remove(key); + if (toResume != null) { + priority = toResume.item1; + task = toResume.item2; + completer = task.completer; + } else { + completer = Completer(); + task = _Task( + () async { + try { + completer.complete(await platformCall()); + } catch (error, stackTrace) { + completer.completeError(error, stackTrace); + } + _runningQueue.remove(key); + _pickNext(); + }, + completer, + ); } - var completer = task?.completer ?? Completer(); - task ??= _Task( - () async { - if (debugLabel != null) debugPrint('$runtimeType $debugLabel start'); - try { - completer.complete(await platformCall()); - } catch (error, stackTrace) { - completer.completeError(error, stackTrace); - } - if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed'); - _runningQueue.removeWhere((task) => task.key == key); - _pickNext(); - }, - completer, - key, - ); - _getQueue(priority).addLast(task); + _getQueue(priority)[key] = task; _pickNext(); return completer.future; } Future resume(Object key) { - final priorityTask = _paused.remove(key); - if (priorityTask == null) return null; - final priority = priorityTask.item1; - final task = priorityTask.item2; - _getQueue(priority).addLast(task); - _pickNext(); - return task.completer.future; + final toResume = _paused.remove(key); + if (toResume != null) { + final priority = toResume.item1; + final task = toResume.item2; + _getQueue(priority)[key] = task; + _pickNext(); + return task.completer.future; + } else { + return null; + } } - Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>()); + LinkedHashMap _getQueue(int priority) => _queues.putIfAbsent(priority, () => LinkedHashMap()); void _pickNext() { _notifyQueueState(); if (_runningQueue.length >= concurrentTaskMax) return; final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value; - final task = queue?.removeFirst(); - if (task != null) { - _runningQueue.addLast(task); + if (queue != null && queue.isNotEmpty) { + final key = queue.keys.first; + final task = queue.remove(key); + _runningQueue[key] = task; task.callback(); } } @@ -80,14 +82,11 @@ class ServicePolicy { bool _takeOut(Object key, Iterable priorities, void Function(int priority, _Task task) action) { var out = false; priorities.forEach((priority) { - final queue = _getQueue(priority); - final tasks = queue.where((task) => task.key == key).toList(); - tasks.forEach((task) { - if (queue.remove(task)) { - out = true; - action(priority, task); - } - }); + final task = _getQueue(priority).remove(key); + if (task != null) { + out = true; + action(priority, task); + } }); return out; } @@ -106,16 +105,15 @@ class ServicePolicy { if (!_queueStreamController.hasListener) return; final queueByPriority = Map.fromEntries(_queues.entries.map((kv) => MapEntry(kv.key, kv.value.length))); - _queueStreamController.add(QueueState(queueByPriority, _runningQueue.length)); + _queueStreamController.add(QueueState(queueByPriority, _runningQueue.length, _paused.length)); } } class _Task { final VoidCallback callback; final Completer completer; - final Object key; - const _Task(this.callback, this.completer, this.key); + const _Task(this.callback, this.completer); } class CancelledException {} @@ -131,7 +129,7 @@ class ServiceCallPriority { class QueueState { final Map queueByPriority; - final int runningQueue; + final int runningCount, pausedCount; - const QueueState(this.queueByPriority, this.runningQueue); + const QueueState(this.queueByPriority, this.runningCount, this.pausedCount); } diff --git a/lib/widgets/debug/overlay.dart b/lib/widgets/debug/overlay.dart index 6fc169a14..463aff4a0 100644 --- a/lib/widgets/debug/overlay.dart +++ b/lib/widgets/debug/overlay.dart @@ -24,7 +24,8 @@ class DebugTaskQueueOverlay extends StatelessWidget { final queuedEntries = >[]; if (snapshot.hasData) { final state = snapshot.data; - queuedEntries.add(MapEntry('run', state.runningQueue)); + queuedEntries.add(MapEntry('run', state.runningCount)); + queuedEntries.add(MapEntry('paused', state.pausedCount)); queuedEntries.addAll(state.queueByPriority.entries.map((kv) => MapEntry(kv.key.toString(), kv.value))); } queuedEntries.sort((a, b) => a.key.compareTo(b.key));