tiling: task management

debug: task queue overlay
This commit is contained in:
Thibault Deckers 2020-11-07 19:48:46 +09:00
parent ac4f6d344e
commit b86faea060
10 changed files with 281 additions and 112 deletions

View file

@ -193,9 +193,11 @@ class ImageFileService {
}
}
static bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]);
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]);
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
static Future<T> resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
try {

View file

@ -7,10 +7,13 @@ import 'package:tuple/tuple.dart';
final ServicePolicy servicePolicy = ServicePolicy._private();
class ServicePolicy {
final StreamController<QueueState> _queueStreamController = StreamController<QueueState>.broadcast();
final Map<Object, Tuple2<int, _Task>> _paused = {};
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap();
_Task _running;
Stream<QueueState> get queueStream => _queueStreamController.stream;
ServicePolicy._private();
Future<T> call<T>(
@ -60,6 +63,7 @@ class ServicePolicy {
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>());
void _pickNext() {
_notifyQueueState();
if (_running != null) return;
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
_running = queue?.removeFirst();
@ -90,6 +94,13 @@ class ServicePolicy {
}
bool isPaused(Object key) => _paused.containsKey(key);
void _notifyQueueState() {
if (!_queueStreamController.hasListener) return;
final queueByPriority = Map.fromEntries(_queues.entries.map((kv) => MapEntry(kv.key, kv.value.length)));
_queueStreamController.add(QueueState(queueByPriority));
}
}
class _Task {
@ -110,3 +121,9 @@ class ServiceCallPriority {
static const int getMetadata = 1000;
static const int getLocation = 1000;
}
class QueueState {
final Map<int, int> queueByPriority;
const QueueState(this.queueByPriority);
}

View file

@ -71,8 +71,6 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
_pauseProvider();
}
bool get isSupported => entry.canDecode;
void _initProvider() {
if (!entry.canDecode) return;

View file

@ -0,0 +1,124 @@
import 'dart:async';
import 'dart:ui' as ui show Codec;
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class RegionProvider extends ImageProvider<RegionProviderKey> {
final RegionProviderKey key;
RegionProvider(this.key) : assert(key != null);
@override
Future<RegionProviderKey> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<RegionProviderKey>(key);
}
@override
ImageStreamCompleter load(RegionProviderKey key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield ErrorDescription('uri=${key.uri}, rect=${key.rect}');
},
);
}
Future<ui.Codec> _loadAsync(RegionProviderKey key, DecoderCallback decode) async {
final uri = key.uri;
final mimeType = key.mimeType;
try {
final bytes = await ImageFileService.getRegion(
uri,
mimeType,
key.rotationDegrees,
key.isFlipped,
key.sampleSize,
key.rect,
taskKey: key,
);
if (bytes == null) {
throw StateError('$uri ($mimeType) region loading failed');
}
return await decode(bytes);
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType region decoding failed');
}
}
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, RegionProviderKey key, ImageErrorListener handleError) {
ImageFileService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError);
}
void pause() => ImageFileService.cancelRegion(key);
}
class RegionProviderKey {
final String uri, mimeType;
final int rotationDegrees, sampleSize;
final bool isFlipped;
final Rect rect;
final double scale;
const RegionProviderKey({
@required this.uri,
@required this.mimeType,
@required this.rotationDegrees,
@required this.isFlipped,
@required this.sampleSize,
@required this.rect,
this.scale = 1.0,
}) : assert(uri != null),
assert(mimeType != null),
assert(rotationDegrees != null),
assert(isFlipped != null),
assert(sampleSize != null),
assert(rect != null),
assert(scale != null);
// do not store the entry as it is, because the key should be constant
// but the entry attributes may change over time
factory RegionProviderKey.fromEntry(
ImageEntry entry, {
@required int sampleSize,
@required Rect rect,
}) {
return RegionProviderKey(
uri: entry.uri,
mimeType: entry.mimeType,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
sampleSize: sampleSize,
rect: rect,
);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.rect == rect && other.scale == scale;
}
@override
int get hashCode => hashValues(
uri,
mimeType,
rotationDegrees,
isFlipped,
mimeType,
sampleSize,
rect,
scale,
);
@override
String toString() {
return 'RegionProviderKey(uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, rect=$rect, scale=$scale)';
}
}

View file

@ -30,8 +30,8 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
}
Future<ui.Codec> _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async {
var uri = key.uri;
var mimeType = key.mimeType;
final uri = key.uri;
final mimeType = key.mimeType;
try {
final bytes = await ImageFileService.getThumbnail(
uri,
@ -55,7 +55,7 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, ThumbnailProviderKey key, ImageErrorListener handleError) {
ImageFileService.resumeThumbnail(key);
ImageFileService.resumeLoading(key);
super.resolveStreamForKey(configuration, stream, key, handleError);
}
@ -105,7 +105,15 @@ class ThumbnailProviderKey {
}
@override
int get hashCode => hashValues(uri, mimeType, dateModifiedSecs, rotationDegrees, isFlipped, extent, scale);
int get hashCode => hashValues(
uri,
mimeType,
dateModifiedSecs,
rotationDegrees,
isFlipped,
extent,
scale,
);
@override
String toString() {

View file

@ -1,81 +0,0 @@
import 'dart:async';
import 'dart:ui' as ui show Codec;
import 'package:aves/services/image_file_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:pedantic/pedantic.dart';
class UriRegion extends ImageProvider<UriRegion> {
const UriRegion({
@required this.uri,
@required this.mimeType,
@required this.rotationDegrees,
@required this.isFlipped,
@required this.sampleSize,
@required this.rect,
this.scale = 1.0,
}) : assert(uri != null),
assert(scale != null);
final String uri, mimeType;
final int rotationDegrees, sampleSize;
final bool isFlipped;
final Rect rect;
final double scale;
@override
Future<UriRegion> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<UriRegion>(this);
}
@override
ImageStreamCompleter load(UriRegion key, DecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode, chunkEvents),
scale: key.scale,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription('uri=$uri, mimeType=$mimeType');
},
);
}
Future<ui.Codec> _loadAsync(UriRegion key, DecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
assert(key == this);
try {
final bytes = await ImageFileService.getRegion(
uri,
mimeType,
rotationDegrees,
isFlipped,
sampleSize,
rect,
);
if (bytes == null) {
throw StateError('$uri ($mimeType) loading failed');
}
return await decode(bytes);
} catch (error) {
debugPrint('$runtimeType _loadAsync failed with mimeType=$mimeType, uri=$uri, error=$error');
throw StateError('$mimeType decoding failed');
} finally {
unawaited(chunkEvents.close());
}
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is UriRegion && other.uri == uri && other.sampleSize == sampleSize && other.rect == rect && other.scale == scale;
}
@override
int get hashCode => hashValues(uri, sampleSize, rect, scale);
@override
String toString() => '${objectRuntimeType(this, 'UriRegion')}(uri=$uri, mimeType=$mimeType, scale=$scale)';
}

View file

@ -6,6 +6,7 @@ import 'package:aves/widgets/debug/android_env.dart';
import 'package:aves/widgets/debug/cache.dart';
import 'package:aves/widgets/debug/database.dart';
import 'package:aves/widgets/debug/firebase.dart';
import 'package:aves/widgets/debug/overlay.dart';
import 'package:aves/widgets/debug/settings.dart';
import 'package:aves/widgets/debug/storage.dart';
import 'package:aves/widgets/fullscreen/info/common.dart';
@ -26,6 +27,8 @@ class AppDebugPage extends StatefulWidget {
class AppDebugPageState extends State<AppDebugPage> {
List<ImageEntry> get entries => widget.source.rawEntries;
static OverlayEntry _taskQueueOverlayEntry;
@override
Widget build(BuildContext context) {
return MediaQueryDataProvider(
@ -70,6 +73,22 @@ class AppDebugPageState extends State<AppDebugPage> {
divisions: 9,
label: '$timeDilation',
),
SwitchListTile(
value: _taskQueueOverlayEntry != null,
onChanged: (v) {
_taskQueueOverlayEntry?.remove();
if (v) {
_taskQueueOverlayEntry = OverlayEntry(
builder: (context) => DebugTaskQueueOverlay(),
);
Overlay.of(context).insert(_taskQueueOverlayEntry);
} else {
_taskQueueOverlayEntry = null;
}
setState(() {});
},
title: Text('Show tasks overlay'),
),
Divider(),
Padding(
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),

View file

@ -0,0 +1,39 @@
import 'package:aves/services/service_policy.dart';
import 'package:flutter/material.dart';
class DebugTaskQueueOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: DefaultTextStyle(
style: TextStyle(),
child: Align(
alignment: AlignmentDirectional.bottomStart,
child: SafeArea(
child: Container(
color: Colors.indigo[900].withAlpha(0xCC),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
padding: EdgeInsets.all(8),
child: StreamBuilder<QueueState>(
stream: servicePolicy.queueStream,
builder: (context, snapshot) {
if (snapshot.hasError) return SizedBox.shrink();
final queuedEntries = (snapshot.hasData ? snapshot.data.queueByPriority.entries.toList() : []);
queuedEntries.sort((a, b) => a.key.compareTo(b.key));
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(queuedEntries.map((kv) => '${kv.key}: ${kv.value}').join(', ')),
],
);
}),
),
),
),
),
);
}
}

View file

@ -2,7 +2,7 @@ import 'dart:math';
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/image_providers/uri_region_provider.dart';
import 'package:aves/widgets/common/image_providers/region_provider.dart';
import 'package:aves/widgets/fullscreen/image_view.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -99,7 +99,7 @@ class _TiledImageViewState extends State<TiledImageView> {
);
final viewRect = (viewOrigin & viewportSize).inflate(preFetchMargin);
final tiles = <Widget>[];
final tiles = <RegionTile>[];
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
final layerRegionSize = Size.square(_tileSide * sampleSize);
@ -159,7 +159,7 @@ class _TiledImageViewState extends State<TiledImageView> {
}
}
class RegionTile extends StatelessWidget {
class RegionTile extends StatefulWidget {
final ImageEntry entry;
// `tileRect` uses Flutter view coordinates
@ -174,35 +174,67 @@ class RegionTile extends StatelessWidget {
@required this.sampleSize,
});
@override
_RegionTileState createState() => _RegionTileState();
}
class _RegionTileState extends State<RegionTile> {
RegionProvider _provider;
ImageEntry get entry => widget.entry;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(RegionTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != widget.entry || oldWidget.tileRect != widget.tileRect || oldWidget.sampleSize != widget.sampleSize || oldWidget.sampleSize != widget.sampleSize) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(RegionTile widget) {
_initProvider();
}
void _unregisterWidget(RegionTile widget) {
_pauseProvider();
}
void _initProvider() {
if (!entry.canDecode) return;
_provider = RegionProvider(RegionProviderKey.fromEntry(
entry,
sampleSize: widget.sampleSize,
rect: widget.regionRect,
));
}
void _pauseProvider() => _provider?.pause();
@override
Widget build(BuildContext context) {
final tileRect = widget.tileRect;
Widget child = Image(
image: UriRegion(
uri: entry.uri,
mimeType: entry.mimeType,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
sampleSize: sampleSize,
rect: regionRect,
),
image: _provider,
width: tileRect.width,
height: tileRect.height,
fit: BoxFit.fill,
// TODO TLAD remove when done with tiling
// color: Color.fromARGB((0xff / sampleSize).floor(), 0, 0, 0xff),
// colorBlendMode: BlendMode.color,
);
// child = Container(
// foregroundDecoration: BoxDecoration(
// border: Border.all(
// color: Colors.cyan,
// ),
// ),
// // child: Text('$sampleSize'),
// child: child,
// );
// apply EXIF orientation
final quarterTurns = entry.rotationDegrees ~/ 90;
if (entry.isFlipped) {
@ -230,4 +262,12 @@ class RegionTile extends StatelessWidget {
child: child,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('contentId', widget.entry.contentId));
properties.add(IntProperty('sampleSize', widget.sampleSize));
properties.add(DiagnosticsProperty<Rect>('regionRect', widget.regionRect));
}
}

View file

@ -33,6 +33,9 @@ version: 1.2.5+31
# - does not support AC3 (by default, but possible by custom build)
# - can play if only the video or audio stream is supported
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter