perf: improved task pause/resume

This commit is contained in:
Thibault Deckers 2021-02-02 19:54:28 +09:00
parent e4ed5ef751
commit e02593def3
4 changed files with 54 additions and 62 deletions

View file

@ -21,7 +21,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) { ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: key.scale, scale: 1.0,
informationCollector: () sync* { informationCollector: () sync* {
yield ErrorDescription('uri=${key.uri}, pageId=${key.pageId}, mimeType=${key.mimeType}, extent=${key.extent}'); 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 int pageId, rotationDegrees;
final bool isFlipped; final bool isFlipped;
final int dateModifiedSecs; final int dateModifiedSecs;
final double extent, scale; final double extent;
const ThumbnailProviderKey({ const ThumbnailProviderKey({
@required this.uri, @required this.uri,
@ -79,33 +79,27 @@ class ThumbnailProviderKey {
@required this.isFlipped, @required this.isFlipped,
@required this.dateModifiedSecs, @required this.dateModifiedSecs,
this.extent = 0, this.extent = 0,
this.scale = 1,
}) : assert(uri != null), }) : assert(uri != null),
assert(mimeType != null), assert(mimeType != null),
assert(rotationDegrees != null), assert(rotationDegrees != null),
assert(isFlipped != null), assert(isFlipped != null),
assert(dateModifiedSecs != null), assert(dateModifiedSecs != null),
assert(extent != null), assert(extent != null);
assert(scale != null);
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false; 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 @override
int get hashCode => hashValues( int get hashCode => hashValues(
uri, uri,
mimeType,
pageId, pageId,
rotationDegrees,
isFlipped,
dateModifiedSecs, dateModifiedSecs,
extent, extent,
scale,
); );
@override @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}';
} }

View file

@ -204,7 +204,6 @@ class ImageFileService {
} }
return null; return null;
}, },
// debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}',
priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail),
key: taskKey, key: taskKey,
); );

View file

@ -9,8 +9,8 @@ final ServicePolicy servicePolicy = ServicePolicy._private();
class ServicePolicy { class ServicePolicy {
final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast(); final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast();
final Map<Object, Tuple2<int, _Task>> _paused = {}; final Map<Object, Tuple2<int, _Task>> _paused = {};
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap(); final SplayTreeMap<int, LinkedHashMap<Object, _Task>> _queues = SplayTreeMap();
final Queue<_Task> _runningQueue = Queue(); final LinkedHashMap<Object, _Task> _runningQueue = LinkedHashMap();
// magic number // magic number
static const concurrentTaskMax = 4; static const concurrentTaskMax = 4;
@ -22,57 +22,59 @@ class ServicePolicy {
Future<T> call<T>( Future<T> call<T>(
Future<T> Function() platformCall, { Future<T> Function() platformCall, {
int priority = ServiceCallPriority.normal, int priority = ServiceCallPriority.normal,
String debugLabel,
Object key, Object key,
}) { }) {
Completer<T> completer;
_Task task; _Task task;
key ??= platformCall.hashCode; key ??= platformCall.hashCode;
final priorityTask = _paused.remove(key); final toResume = _paused.remove(key);
if (priorityTask != null) { if (toResume != null) {
debugPrint('resume task with key=$key'); priority = toResume.item1;
priority = priorityTask.item1; task = toResume.item2;
task = priorityTask.item2; completer = task.completer;
} else {
completer = Completer<T>();
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<T>(); _getQueue(priority)[key] = task;
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);
_pickNext(); _pickNext();
return completer.future; return completer.future;
} }
Future<T> resume<T>(Object key) { Future<T> resume<T>(Object key) {
final priorityTask = _paused.remove(key); final toResume = _paused.remove(key);
if (priorityTask == null) return null; if (toResume != null) {
final priority = priorityTask.item1; final priority = toResume.item1;
final task = priorityTask.item2; final task = toResume.item2;
_getQueue(priority).addLast(task); _getQueue(priority)[key] = task;
_pickNext(); _pickNext();
return task.completer.future; return task.completer.future;
} else {
return null;
}
} }
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>()); LinkedHashMap<Object, _Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => LinkedHashMap());
void _pickNext() { void _pickNext() {
_notifyQueueState(); _notifyQueueState();
if (_runningQueue.length >= concurrentTaskMax) return; if (_runningQueue.length >= concurrentTaskMax) return;
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value; final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
final task = queue?.removeFirst(); if (queue != null && queue.isNotEmpty) {
if (task != null) { final key = queue.keys.first;
_runningQueue.addLast(task); final task = queue.remove(key);
_runningQueue[key] = task;
task.callback(); task.callback();
} }
} }
@ -80,14 +82,11 @@ class ServicePolicy {
bool _takeOut(Object key, Iterable<int> priorities, void Function(int priority, _Task task) action) { bool _takeOut(Object key, Iterable<int> priorities, void Function(int priority, _Task task) action) {
var out = false; var out = false;
priorities.forEach((priority) { priorities.forEach((priority) {
final queue = _getQueue(priority); final task = _getQueue(priority).remove(key);
final tasks = queue.where((task) => task.key == key).toList(); if (task != null) {
tasks.forEach((task) { out = true;
if (queue.remove(task)) { action(priority, task);
out = true; }
action(priority, task);
}
});
}); });
return out; return out;
} }
@ -106,16 +105,15 @@ class ServicePolicy {
if (!_queueStreamController.hasListener) return; if (!_queueStreamController.hasListener) return;
final queueByPriority = Map.fromEntries(_queues.entries.map((kv) => MapEntry(kv.key, kv.value.length))); 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 { class _Task {
final VoidCallback callback; final VoidCallback callback;
final Completer completer; final Completer completer;
final Object key;
const _Task(this.callback, this.completer, this.key); const _Task(this.callback, this.completer);
} }
class CancelledException {} class CancelledException {}
@ -131,7 +129,7 @@ class ServiceCallPriority {
class QueueState { class QueueState {
final Map<int, int> queueByPriority; final Map<int, int> queueByPriority;
final int runningQueue; final int runningCount, pausedCount;
const QueueState(this.queueByPriority, this.runningQueue); const QueueState(this.queueByPriority, this.runningCount, this.pausedCount);
} }

View file

@ -24,7 +24,8 @@ class DebugTaskQueueOverlay extends StatelessWidget {
final queuedEntries = <MapEntry<dynamic, int>>[]; final queuedEntries = <MapEntry<dynamic, int>>[];
if (snapshot.hasData) { if (snapshot.hasData) {
final state = snapshot.data; 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.addAll(state.queueByPriority.entries.map((kv) => MapEntry(kv.key.toString(), kv.value)));
} }
queuedEntries.sort((a, b) => a.key.compareTo(b.key)); queuedEntries.sort((a, b) => a.key.compareTo(b.key));