Bladeren bron

Improve logic to load thumbnails

Vishnu Mohandas 4 jaren geleden
bovenliggende
commit
d5f9f3e497
3 gewijzigde bestanden met toevoegingen van 94 en 85 verwijderingen
  1. 2 0
      lib/core/errors.dart
  2. 26 14
      lib/ui/thumbnail_widget.dart
  3. 66 71
      lib/utils/thumbnail_util.dart

+ 2 - 0
lib/core/errors.dart

@@ -15,3 +15,5 @@ class UserCancelledUploadError extends Error {}
 class LockAlreadyAcquiredError extends Error {}
 
 class UnauthorizedError extends Error {}
+
+class RequestCancelledError extends Error{}

+ 26 - 14
lib/ui/thumbnail_widget.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:photos/core/cache/image_cache.dart';
 import 'package:photos/core/cache/thumbnail_cache.dart';
+import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
@@ -9,7 +10,6 @@ import 'package:logging/logging.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/ui/common_elements.dart';
-import 'package:photos/utils/file_util.dart';
 import 'package:photos/utils/thumbnail_util.dart';
 
 class ThumbnailWidget extends StatefulWidget {
@@ -207,10 +207,11 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
     }
   }
 
-  void _getThumbnailFromServer() {
-    getThumbnailFromServer(widget.file).then((file) async {
+  void _getThumbnailFromServer() async {
+    try {
+      final thumbnail = await getThumbnailFromServer(widget.file);
       if (mounted) {
-        final imageProvider = Image.file(file).image;
+        final imageProvider = Image.file(thumbnail).image;
         precacheImage(imageProvider, context).then((value) {
           if (mounted) {
             setState(() {
@@ -218,24 +219,35 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
               _hasLoadedThumbnail = true;
             });
           }
-        }).catchError((e) {
-          _logger.severe("Could not load image " + widget.file.toString());
-          _encounteredErrorLoadingThumbnail = true;
         });
       }
-    });
+    } catch (e) {
+      if (e is RequestCancelledError) {
+        if (mounted) {
+          _logger.info("Thumbnail request was aborted although it is in view, will retry");
+          _reset();
+        }
+      } else {
+        _logger.severe("Could not load image " + widget.file.toString(), e);
+        _encounteredErrorLoadingThumbnail = true;
+      }
+    }
   }
 
   @override
   void didUpdateWidget(ThumbnailWidget oldWidget) {
     super.didUpdateWidget(oldWidget);
     if (widget.file.generatedID != oldWidget.file.generatedID) {
-      setState(() {
-        _hasLoadedThumbnail = false;
-        _isLoadingThumbnail = false;
-        _encounteredErrorLoadingThumbnail = false;
-        _imageProvider = null;
-      });
+      _reset();
     }
   }
+
+  void _reset() {
+    setState(() {
+      _hasLoadedThumbnail = false;
+      _isLoadingThumbnail = false;
+      _encounteredErrorLoadingThumbnail = false;
+      _imageProvider = null;
+    });
+  }
 }

+ 66 - 71
lib/utils/thumbnail_util.dart

@@ -10,46 +10,43 @@ import 'package:photos/core/cache/image_cache.dart';
 import 'package:photos/core/cache/thumbnail_cache_manager.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
+import 'package:photos/core/errors.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/utils/crypto_util.dart';
 import 'package:photos/utils/file_util.dart';
 
 final _logger = Logger("ThumbnailUtil");
-final _thumbnailQueue = LinkedHashMap<int, FileDownloadItem>();
-int _currentlyDownloading = 0;
-const int kMaximumConcurrentDownloads = 500;
+final _map = LinkedHashMap<int, FileDownloadItem>();
+final _queue = Queue<int>();
+const int kMaximumConcurrentDownloads = 2500;
 
 class FileDownloadItem {
   final File file;
   final Completer<io.File> completer;
-  DownloadStatus status;
+  final CancelToken cancelToken;
 
-  FileDownloadItem(
-    this.file,
-    this.completer, {
-    this.status = DownloadStatus.not_started,
-  });
-}
-
-enum DownloadStatus {
-  not_started,
-  in_progress,
+  FileDownloadItem(this.file, this.completer, this.cancelToken);
 }
 
 Future<io.File> getThumbnailFromServer(File file) async {
-  return ThumbnailCacheManager.instance
-      .getFileFromCache(file.getThumbnailUrl())
-      .then((info) {
+  return ThumbnailCacheManager.instance.getFileFromCache("asd").then((info) {
     if (info == null) {
-      if (!_thumbnailQueue.containsKey(file.uploadedFileID)) {
-        final completer = Completer<io.File>();
-        _thumbnailQueue[file.uploadedFileID] =
-            FileDownloadItem(file, completer);
-        _pollQueue();
-        return completer.future;
+      if (!_map.containsKey(file.uploadedFileID)) {
+        if (_queue.length == kMaximumConcurrentDownloads) {
+          final id = _queue.removeFirst();
+          final item = _map.remove(id);
+          item.cancelToken.cancel();
+          item.completer.completeError(RequestCancelledError());
+        }
+        final item =
+            FileDownloadItem(file, Completer<io.File>(), CancelToken());
+        _map[file.uploadedFileID] = item;
+        _queue.add(file.uploadedFileID);
+        _downloadItem(item);
+        return item.completer.future;
       } else {
-        return _thumbnailQueue[file.uploadedFileID].completer.future;
+        return _map[file.uploadedFileID].completer.future;
       }
     } else {
       ThumbnailFileLruCache.put(file, info.file);
@@ -59,71 +56,69 @@ Future<io.File> getThumbnailFromServer(File file) async {
 }
 
 void removePendingGetThumbnailRequestIfAny(File file) {
-  if (_thumbnailQueue[file.uploadedFileID] != null &&
-      _thumbnailQueue[file.uploadedFileID].status ==
-          DownloadStatus.not_started) {
-    _thumbnailQueue.remove(file.uploadedFileID);
+  if (_map.containsKey(file.uploadedFileID)) {
+    final item = _map.remove(file.uploadedFileID);
+    item.cancelToken.cancel();
+    _queue.removeWhere((element) => element == file.uploadedFileID);
   }
 }
 
-void _pollQueue() async {
-  if (_thumbnailQueue.length > 0 &&
-      _currentlyDownloading < kMaximumConcurrentDownloads) {
-    final firstPendingEntry = _thumbnailQueue.entries.firstWhere(
-        (entry) => entry.value.status == DownloadStatus.not_started,
-        orElse: () => null);
-    if (firstPendingEntry != null) {
-      final item = firstPendingEntry.value;
-      _currentlyDownloading++;
-      item.status = DownloadStatus.in_progress;
-      try {
-        final data = await _downloadAndDecryptThumbnail(item.file);
-        ThumbnailFileLruCache.put(item.file, data);
-        item.completer.complete(data);
-      } catch (e, s) {
-        _logger.severe(
-            "Failed to download thumbnail " + item.file.toString(), e, s);
-        item.completer.completeError(e);
-      }
-      _currentlyDownloading--;
-      _thumbnailQueue.remove(firstPendingEntry.key);
-      _pollQueue();
-    }
+void _downloadItem(FileDownloadItem item) async {
+  try {
+    await _downloadAndDecryptThumbnail(item);
+  } catch (e, s) {
+    _logger.severe(
+        "Failed to download thumbnail " + item.file.toString(), e, s);
+    item.completer.completeError(e);
   }
+  _queue.removeWhere((element) => element == item.file.uploadedFileID);
+  _map.remove(item.file.uploadedFileID);
 }
 
-Future<io.File> _downloadAndDecryptThumbnail(File file) async {
-  final temporaryPath = Configuration.instance.getTempDirectory() +
-      file.generatedID.toString() +
-      "_thumbnail.decrypted";
-  await Network.instance.getDio().download(
-        file.getThumbnailUrl(),
-        temporaryPath,
-        options: Options(
-          headers: {"X-Auth-Token": Configuration.instance.getToken()},
-        ),
-      );
-  final encryptedFile = io.File(temporaryPath);
+Future<void> _downloadAndDecryptThumbnail(FileDownloadItem item) async {
+  final file = item.file;
+  var encryptedThumbnail;
+  try {
+    encryptedThumbnail = (await Network.instance.getDio().get(
+          file.getThumbnailUrl(),
+          options: Options(
+            headers: {"X-Auth-Token": Configuration.instance.getToken()},
+            responseType: ResponseType.bytes,
+          ),
+          cancelToken: item.cancelToken,
+        )).data;
+  } catch (e) {
+    if (e is DioError && CancelToken.isCancel(e)) {
+      return;
+    }
+    throw e;
+  }
+  if (!_map.containsKey(file.uploadedFileID)) {
+    return;
+  }
   final thumbnailDecryptionKey = decryptFileKey(file);
   var data = CryptoUtil.decryptChaCha(
-    encryptedFile.readAsBytesSync(),
+    encryptedThumbnail,
     thumbnailDecryptionKey,
     Sodium.base642bin(file.thumbnailDecryptionHeader),
   );
   final thumbnailSize = data.length;
   if (thumbnailSize > THUMBNAIL_DATA_LIMIT) {
     data = await compressThumbnail(data);
-    _logger.info("Compressed thumbnail from " +
-        thumbnailSize.toString() +
-        " to " +
-        data.length.toString());
   }
-  encryptedFile.deleteSync();
-  final cachedThumbnail = ThumbnailCacheManager.instance.putFile(
+  final cachedThumbnail = await ThumbnailCacheManager.instance.putFile(
     file.getThumbnailUrl(),
     data,
     eTag: file.getThumbnailUrl(),
     maxAge: Duration(days: 365),
   );
-  return cachedThumbnail;
+  ThumbnailFileLruCache.put(item.file, cachedThumbnail);
+  if (_map.containsKey(file.uploadedFileID)) {
+    try {
+      item.completer.complete(cachedThumbnail);
+    } catch (e) {
+      _logger.severe("Error while completing request for " +
+          file.uploadedFileID.toString());
+    }
+  }
 }