perf: improved task pause/resume
This commit is contained in:
parent
e4ed5ef751
commit
e02593def3
4 changed files with 54 additions and 62 deletions
|
@ -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}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
Loading…
Reference in a new issue