thumbnails: changed cancellation strategy

This commit is contained in:
Thibault Deckers 2020-04-30 10:04:54 +09:00
parent 8dfcdfe052
commit 157fc60322
6 changed files with 97 additions and 82 deletions

View file

@ -226,7 +226,6 @@ class ImageEntry {
final addresses = await servicePolicy.call(
() => Geocoder.local.findAddressesFromCoordinates(coordinates),
priority: ServiceCallPriority.background,
debugLabel: 'findAddressesFromCoordinates-$path',
);
if (addresses != null && addresses.isNotEmpty) {
final address = addresses.first;

View file

@ -13,6 +13,8 @@ class ImageFileService {
static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
static const thumbnailPriority = ServiceCallPriority.asap;
static Future<void> getImageEntries() async {
try {
await platform.invokeMethod('getImageEntries');
@ -55,7 +57,7 @@ class ImageFileService {
return Future.sync(() => Uint8List(0));
}
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object cancellationKey}) {
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object taskKey}) {
return servicePolicy.call(
() async {
if (width > 0 && height > 0) {
@ -72,12 +74,15 @@ class ImageFileService {
}
return Uint8List(0);
},
priority: ServiceCallPriority.asap,
debugLabel: 'getThumbnail-${entry.path}',
cancellationKey: cancellationKey,
priority: thumbnailPriority,
key: taskKey,
);
}
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, thumbnailPriority);
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey, thumbnailPriority);
static Stream<ImageOpEvent> delete(List<ImageEntry> entries) {
try {
return opChannel.receiveBroadcastStream(<String, dynamic>{

View file

@ -51,7 +51,6 @@ class MetadataService {
return null;
},
priority: ServiceCallPriority.background,
debugLabel: 'getCatalogMetadata-${entry.path}',
);
}

View file

@ -6,14 +6,12 @@ import 'package:flutter/foundation.dart';
final ServicePolicy servicePolicy = ServicePolicy._private();
class ServicePolicy {
final Queue<_Task> _asapQueue, _normalQueue, _backgroundQueue;
final Map<Object, _Task> _paused = {};
final Queue<_Task> _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue();
List<Queue<_Task>> _queues;
_Task _running;
ServicePolicy._private()
: _asapQueue = Queue(),
_normalQueue = Queue(),
_backgroundQueue = Queue() {
ServicePolicy._private() {
_queues = [_asapQueue, _normalQueue, _backgroundQueue];
}
@ -21,8 +19,39 @@ class ServicePolicy {
Future<T> Function() platformCall, {
ServiceCallPriority priority = ServiceCallPriority.normal,
String debugLabel,
Object cancellationKey,
Object key,
}) {
var task = _paused.remove(key);
if (task != null) {
debugPrint('resume task with key=$key');
}
var completer = task?.completer ?? Completer<T>();
task ??= _Task(
() async {
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
final result = await platformCall();
completer.complete(result);
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
_running = null;
_pickNext();
},
completer,
key,
);
_getQueue(priority).addLast(task);
_pickNext();
return completer.future;
}
Future<T> resume<T>(Object key, ServiceCallPriority priority) {
var task = _paused.remove(key);
if (task == null) return null;
_getQueue(priority).addLast(task);
_pickNext();
return task.completer.future;
}
Queue<_Task> _getQueue(ServiceCallPriority priority) {
Queue<_Task> queue;
switch (priority) {
case ServiceCallPriority.asap:
@ -36,23 +65,7 @@ class ServicePolicy {
queue = _normalQueue;
break;
}
final completer = Completer<T>();
final wrapped = _Task(
() async {
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
final result = await platformCall();
completer.complete(result);
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
_running = null;
_pickNext();
},
completer,
cancellationKey,
);
queue.addLast(wrapped);
_pickNext();
return completer.future;
return queue;
}
void _pickNext() {
@ -62,25 +75,41 @@ class ServicePolicy {
_running?.callback?.call();
}
bool cancel(Object cancellationKey) {
bool cancel(Object key, ServiceCallPriority priority) {
var cancelled = false;
final tasks = _queues.expand((q) => q.where((task) => task.cancellationKey == cancellationKey)).toList();
tasks.forEach((task) => _queues.forEach((q) {
if (q.remove(task)) {
cancelled = true;
task.completer.completeError(CancelledException());
}
}));
final queue = _getQueue(priority);
final tasks = queue.where((task) => task.key == key).toList();
tasks.forEach((task) {
if (queue.remove(task)) {
cancelled = true;
task.completer.completeError(CancelledException());
}
});
return cancelled;
}
bool pause(Object key, ServiceCallPriority priority) {
var paused = false;
final queue = _getQueue(priority);
final tasks = queue.where((task) => task.key == key).toList();
tasks.forEach((task) {
if (queue.remove(task)) {
paused = true;
_paused.putIfAbsent(key, () => task);
}
});
return paused;
}
bool isPaused(Object key) => _paused.containsKey(key);
}
class _Task {
final VoidCallback callback;
final Completer completer;
final Object cancellationKey;
final Object key;
const _Task(this.callback, this.completer, this.cancellationKey);
const _Task(this.callback, this.completer, this.key);
}
class CancelledException {}

View file

@ -40,20 +40,20 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
void didUpdateWidget(ThumbnailRasterImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != entry) {
_cancelProvider();
_pauseProvider();
_initProvider();
}
}
@override
void dispose() {
_cancelProvider();
_pauseProvider();
super.dispose();
}
void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
void _cancelProvider() => _imageProvider?.cancel();
void _pauseProvider() => _imageProvider?.pause();
@override
Widget build(BuildContext context) {

View file

@ -3,10 +3,8 @@ import 'dart:ui' as ui show Codec;
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/services/service_policy.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
ThumbnailProvider({
@ -15,30 +13,34 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
this.scale = 1.0,
}) : assert(entry != null),
assert(extent != null),
assert(scale != null);
assert(scale != null) {
_cancellationKey = _buildKey(ImageConfiguration.empty);
}
final ImageEntry entry;
final double extent;
final double scale;
final Object _cancellationKey = Uuid();
Object _cancellationKey;
@override
Future<ThumbnailProviderKey> obtainKey(ImageConfiguration configuration) {
// configuration can be empty (e.g. when obtaining key for eviction)
// so we do not compute the target width/height here
// and pass it to the key, to use it later for image loading
return SynchronousFuture<ThumbnailProviderKey>(ThumbnailProviderKey(
entry: entry,
extent: extent,
devicePixelRatio: configuration.devicePixelRatio,
scale: scale,
));
return SynchronousFuture<ThumbnailProviderKey>(_buildKey(configuration));
}
ThumbnailProviderKey _buildKey(ImageConfiguration configuration) => ThumbnailProviderKey(
entry: entry,
extent: extent,
devicePixelRatio: configuration.devicePixelRatio,
scale: scale,
);
@override
ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) {
return CancellableMultiFrameImageStreamCompleter(
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
@ -49,15 +51,17 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
final dimPixels = (extent * key.devicePixelRatio).round();
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, cancellationKey: _cancellationKey);
final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, taskKey: _cancellationKey);
return await decode(bytes ?? Uint8List(0));
}
Future<void> cancel() async {
if (servicePolicy.cancel(_cancellationKey)) {
await evict();
}
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, handleError) {
ImageFileService.resumeThumbnail(_cancellationKey);
super.resolveStreamForKey(configuration, stream, key, handleError);
}
void pause() => ImageFileService.cancelThumbnail(_cancellationKey);
}
class ThumbnailProviderKey {
@ -76,35 +80,14 @@ class ThumbnailProviderKey {
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is ThumbnailProviderKey && other.entry.uri == entry.uri && other.extent == extent && other.scale == scale;
return other is ThumbnailProviderKey && other.entry.contentId == entry.contentId && other.extent == extent && other.scale == scale;
}
@override
int get hashCode => hashValues(entry.uri, extent, scale);
int get hashCode => hashValues(entry.contentId, extent, scale);
@override
String toString() {
return 'ThumbnailProviderKey{uri=${entry.uri}, extent=$extent, scale=$scale}';
}
}
class CancellableMultiFrameImageStreamCompleter extends MultiFrameImageStreamCompleter {
CancellableMultiFrameImageStreamCompleter({
@required Future<ui.Codec> codec,
@required double scale,
Stream<ImageChunkEvent> chunkEvents,
InformationCollector informationCollector,
}) : super(
codec: codec,
scale: scale,
chunkEvents: chunkEvents,
informationCollector: informationCollector,
);
@override
void reportError({DiagnosticsNode context, dynamic exception, StackTrace stack, informationCollector, bool silent = false}) {
// prevent default error reporting in case of planned cancellation
if (exception is CancelledException) return;
super.reportError(context: context, exception: exception, stack: stack, informationCollector: informationCollector, silent: silent);
return 'ThumbnailProviderKey{contentId=${entry.contentId}, extent=$extent, scale=$scale}';
}
}