[mob][debug] To debug thumbnail not loading (#7036)

This commit is contained in:
Ashil
2025-09-02 12:28:34 +05:30
committed by GitHub
5 changed files with 538 additions and 14 deletions

View File

@@ -9,12 +9,20 @@ import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/expandable_menu_item_widget.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import 'package:photos/ui/components/toggle_switch_widget.dart';
import 'package:photos/ui/notification/toast.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/ui/settings/debug/local_thumbnail_config_screen.dart';
import 'package:photos/utils/navigation_util.dart';
class DebugSectionWidget extends StatelessWidget {
class DebugSectionWidget extends StatefulWidget {
const DebugSectionWidget({super.key});
@override
State<DebugSectionWidget> createState() => _DebugSectionWidgetState();
}
class _DebugSectionWidgetState extends State<DebugSectionWidget> {
@override
Widget build(BuildContext context) {
return ExpandableMenuItemWidget(
@@ -27,6 +35,43 @@ class DebugSectionWidget extends StatelessWidget {
Widget _getSectionOptions(BuildContext context) {
return Column(
children: [
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Show local ID over thumbnails",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingWidget: ToggleSwitchWidget(
value: () => localSettings.showLocalIDOverThumbnails,
onChanged: () async {
await localSettings.setShowLocalIDOverThumbnails(
!localSettings.showLocalIDOverThumbnails,
);
setState(() {});
showShortToast(
context,
localSettings.showLocalIDOverThumbnails
? "Local IDs will be shown. Restart app."
: "Local IDs hidden. Restart app.",
);
},
),
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Local thumbnail queue config",
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await routeToPage(
context,
const LocalThumbnailConfigScreen(),
);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(

View File

@@ -0,0 +1,350 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:photos/service_locator.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/notification/toast.dart';
class LocalThumbnailConfigScreen extends StatefulWidget {
const LocalThumbnailConfigScreen({super.key});
@override
State<LocalThumbnailConfigScreen> createState() =>
_LocalThumbnailConfigScreenState();
}
class _LocalThumbnailConfigScreenState
extends State<LocalThumbnailConfigScreen> {
static final Logger _logger = Logger("LocalThumbnailConfigScreen");
late TextEditingController _smallMaxConcurrentController;
late TextEditingController _smallTimeoutController;
late TextEditingController _smallMaxSizeController;
late TextEditingController _largeMaxConcurrentController;
late TextEditingController _largeTimeoutController;
late TextEditingController _largeMaxSizeController;
@override
void initState() {
super.initState();
_initControllers();
}
void _initControllers() {
_smallMaxConcurrentController = TextEditingController(
text: localSettings.smallQueueMaxConcurrent.toString(),
);
_smallTimeoutController = TextEditingController(
text: localSettings.smallQueueTimeoutSeconds.toString(),
);
_smallMaxSizeController = TextEditingController(
text: localSettings.smallQueueMaxSize.toString(),
);
_largeMaxConcurrentController = TextEditingController(
text: localSettings.largeQueueMaxConcurrent.toString(),
);
_largeTimeoutController = TextEditingController(
text: localSettings.largeQueueTimeoutSeconds.toString(),
);
_largeMaxSizeController = TextEditingController(
text: localSettings.largeQueueMaxSize.toString(),
);
}
@override
void dispose() {
_smallMaxConcurrentController.dispose();
_smallTimeoutController.dispose();
_smallMaxSizeController.dispose();
_largeMaxConcurrentController.dispose();
_largeTimeoutController.dispose();
_largeMaxSizeController.dispose();
super.dispose();
}
Future<void> _saveSettings() async {
try {
// Validate and save small queue settings
final smallMaxConcurrent =
int.tryParse(_smallMaxConcurrentController.text);
final smallTimeout = int.tryParse(_smallTimeoutController.text);
final smallMaxSize = int.tryParse(_smallMaxSizeController.text);
// Validate and save large queue settings
final largeMaxConcurrent =
int.tryParse(_largeMaxConcurrentController.text);
final largeTimeout = int.tryParse(_largeTimeoutController.text);
final largeMaxSize = int.tryParse(_largeMaxSizeController.text);
if (smallMaxConcurrent == null ||
smallTimeout == null ||
smallMaxSize == null ||
largeMaxConcurrent == null ||
largeTimeout == null ||
largeMaxSize == null) {
showShortToast(context, "Please enter valid numbers");
return;
}
// Basic validation - just ensure positive numbers
if (smallMaxConcurrent < 1 ||
largeMaxConcurrent < 1 ||
smallTimeout < 1 ||
largeTimeout < 1 ||
smallMaxSize < 1 ||
largeMaxSize < 1) {
showShortToast(
context,
"All values must be positive numbers",
);
return;
}
await localSettings.setSmallQueueMaxConcurrent(smallMaxConcurrent);
await localSettings.setSmallQueueTimeout(smallTimeout);
await localSettings.setSmallQueueMaxSize(smallMaxSize);
await localSettings.setLargeQueueMaxConcurrent(largeMaxConcurrent);
await localSettings.setLargeQueueTimeout(largeTimeout);
await localSettings.setLargeQueueMaxSize(largeMaxSize);
_logger.info(
"Local thumbnail queue settings updated:\n"
"Small Queue - MaxConcurrent: $smallMaxConcurrent, Timeout: ${smallTimeout}s, MaxSize: $smallMaxSize\n"
"Large Queue - MaxConcurrent: $largeMaxConcurrent, Timeout: ${largeTimeout}s, MaxSize: $largeMaxSize",
);
if (mounted) {
showShortToast(
context,
"Settings saved. Restart app to apply changes.",
);
}
} catch (e) {
showShortToast(context, "Error saving settings");
}
}
Future<void> _resetToDefaults() async {
await localSettings.resetThumbnailQueueSettings();
setState(() {
_smallMaxConcurrentController.text = "15";
_smallTimeoutController.text = "60";
_smallMaxSizeController.text = "200";
_largeMaxConcurrentController.text = "5";
_largeTimeoutController.text = "60";
_largeMaxSizeController.text = "200";
});
_logger.info(
"Local thumbnail queue settings reset to defaults:\n"
"Small Queue - MaxConcurrent: 15, Timeout: 60s, MaxSize: 200\n"
"Large Queue - MaxConcurrent: 5, Timeout: 60s, MaxSize: 200",
);
if (mounted) {
showShortToast(
context,
"Reset to defaults. Restart app to apply changes.",
);
}
}
Widget _buildNumberField({
required String label,
required String hint,
required TextEditingController controller,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: getEnteTextTheme(context).body.copyWith(
color: getEnteColorScheme(context).textMuted,
),
),
const SizedBox(height: 4),
TextField(
controller: controller,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
hintText: hint,
hintStyle: getEnteTextTheme(context).body.copyWith(
color: getEnteColorScheme(context).textFaint,
),
filled: true,
fillColor: getEnteColorScheme(context).fillFaint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
style: getEnteTextTheme(context).body,
),
],
),
);
}
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Scaffold(
body: Container(
color: colorScheme.backdropBase,
child: SafeArea(
child: Column(
children: [
const TitleBarTitleWidget(
title: "Local Thumbnail Queue Config",
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Small Local Thumbnail Queue",
style: getEnteTextTheme(context).largeBold,
),
const SizedBox(height: 8),
Text(
"Used when gallery grid has 4 or more columns",
style: getEnteTextTheme(context).small.copyWith(
color: colorScheme.textMuted,
),
),
const SizedBox(height: 16),
_buildNumberField(
label: "Max Concurrent Tasks",
hint: "Default: 15",
controller: _smallMaxConcurrentController,
),
_buildNumberField(
label: "Timeout (seconds)",
hint: "Default: 60",
controller: _smallTimeoutController,
),
_buildNumberField(
label: "Max Queue Size",
hint: "Default: 200",
controller: _smallMaxSizeController,
),
const SizedBox(height: 32),
Text(
"Large Local Thumbnail Queue",
style: getEnteTextTheme(context).largeBold,
),
const SizedBox(height: 8),
Text(
"Used when gallery grid has less than 4 columns",
style: getEnteTextTheme(context).small.copyWith(
color: colorScheme.textMuted,
),
),
const SizedBox(height: 16),
_buildNumberField(
label: "Max Concurrent Tasks",
hint: "Default: 5",
controller: _largeMaxConcurrentController,
),
_buildNumberField(
label: "Timeout (seconds)",
hint: "Default: 60",
controller: _largeTimeoutController,
),
_buildNumberField(
label: "Max Queue Size",
hint: "Default: 200",
controller: _largeMaxSizeController,
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _saveSettings,
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary700,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
"Save Settings",
style: getEnteTextTheme(context).bodyBold.copyWith(
color: Colors.white,
),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: _resetToDefaults,
style: TextButton.styleFrom(
foregroundColor: colorScheme.primary700,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: colorScheme.strokeMuted,
),
),
),
child: const Text(
"Reset to Defaults",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.fillFaint,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: colorScheme.textMuted,
),
const SizedBox(width: 8),
Expanded(
child: Text(
"Changes require app restart to take effect",
style: getEnteTextTheme(context).small.copyWith(
color: colorScheme.textMuted,
),
),
),
],
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@ import 'package:photos/models/file/extensions/file_props.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import 'package:photos/models/file/trash_file.dart';
import 'package:photos/service_locator.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/favorites_service.dart';
import 'package:photos/ui/viewer/file/file_icons_widget.dart';
@@ -98,13 +99,20 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
if (!mounted && _localThumbnailQueueTaskId != null) {
if (widget.thumbnailSize == thumbnailLargeSize) {
largeLocalThumbnailQueue.removeTask(_localThumbnailQueueTaskId!);
_logger.info(
"Cancelled large thumbnail task: $_localThumbnailQueueTaskId",
);
} else if (widget.thumbnailSize == thumbnailSmallSize) {
smallLocalThumbnailQueue.removeTask(_localThumbnailQueueTaskId!);
_logger.info(
"Cancelled small thumbnail task: $_localThumbnailQueueTaskId",
);
}
}
// Cancel request only if the widget has been unmounted
if (!mounted && widget.file.isRemoteFile && !_hasLoadedThumbnail) {
removePendingGetThumbnailRequestIfAny(widget.file);
_logger.info("Cancelled thumbnail request for " + widget.file.tag);
}
});
}
@@ -117,17 +125,42 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
}
}
static final smallLocalThumbnailQueue = TaskQueue<String>(
maxConcurrentTasks: 15,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 200,
);
static final TaskQueue<String> smallLocalThumbnailQueue = _initSmallQueue();
static final TaskQueue<String> largeLocalThumbnailQueue = _initLargeQueue();
static final largeLocalThumbnailQueue = TaskQueue<String>(
maxConcurrentTasks: 5,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 200,
);
static TaskQueue<String> _initSmallQueue() {
final maxConcurrent = localSettings.smallQueueMaxConcurrent;
final timeoutSeconds = localSettings.smallQueueTimeoutSeconds;
final maxSize = localSettings.smallQueueMaxSize;
_logger.info(
"Initializing Small Local Thumbnail Queue - "
"MaxConcurrent: $maxConcurrent, Timeout: ${timeoutSeconds}s, MaxSize: $maxSize",
);
return TaskQueue<String>(
maxConcurrentTasks: maxConcurrent,
taskTimeout: Duration(seconds: timeoutSeconds),
maxQueueSize: maxSize,
);
}
static TaskQueue<String> _initLargeQueue() {
final maxConcurrent = localSettings.largeQueueMaxConcurrent;
final timeoutSeconds = localSettings.largeQueueTimeoutSeconds;
final maxSize = localSettings.largeQueueMaxSize;
_logger.info(
"Initializing Large Local Thumbnail Queue - "
"MaxConcurrent: $maxConcurrent, Timeout: ${timeoutSeconds}s, MaxSize: $maxSize",
);
return TaskQueue<String>(
maxConcurrentTasks: maxConcurrent,
taskTimeout: Duration(seconds: timeoutSeconds),
maxQueueSize: maxSize,
);
}
///Assigned dimension will be the size of a grid item. The size will be
///assigned to the side which is smaller in dimension.
@@ -230,6 +263,32 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
if (widget.shouldShowPinIcon) {
viewChildren.add(const PinOverlayIcon());
}
if (localSettings.showLocalIDOverThumbnails &&
widget.file.localID != null) {
viewChildren.add(
Positioned(
bottom: 4,
left: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: const Color.fromRGBO(0, 0, 0, 0.8),
borderRadius: BorderRadius.circular(4),
),
child: FittedBox(
child: Text(
"${widget.file.localID}",
style: const TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
return Stack(
clipBehavior: Clip.none,
@@ -311,9 +370,14 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
_errorLoadingLocalThumbnail = true;
if (e is WidgetUnmountedException) {
// Widget was unmounted - this is expected behavior
_logger.fine("Thumbnail loading cancelled: widget unmounted");
_logger.fine(
"Thumbnail loading cancelled: widget unmounted for localID: ${widget.file.localID}",
);
} else {
_logger.warning("Could not load thumbnail from disk: ", e);
_logger.warning(
"Could not load thumbnail from disk for localID: ${widget.file.localID}",
e,
);
}
});
}
@@ -343,7 +407,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
);
if (retryAttempts <= _maxLocalThumbnailRetries) {
_logger.warning(
"Error getting local thumbnail for ${widget.file.displayName}, retrying (attempt $retryAttempts) in ${backoff.inMilliseconds} ms",
"Error getting local thumbnail for ${widget.file.displayName} (localID: ${widget.file.localID}) due to ${e.runtimeType}, retrying (attempt $retryAttempts) in ${backoff.inMilliseconds} ms",
e,
);
await Future.delayed(backoff); // Exponential backoff

View File

@@ -41,6 +41,15 @@ class LocalSettings {
"hide_shared_items_from_home_gallery";
static const kCollectionViewType = "collection_view_type";
static const kCollectionSortDirection = "collection_sort_direction";
static const kShowLocalIDOverThumbnails = "show_local_id_over_thumbnails";
// Thumbnail queue configuration keys
static const kSmallQueueMaxConcurrent = "small_queue_max_concurrent";
static const kSmallQueueTimeout = "small_queue_timeout_seconds";
static const kSmallQueueMaxSize = "small_queue_max_size";
static const kLargeQueueMaxConcurrent = "large_queue_max_concurrent";
static const kLargeQueueTimeout = "large_queue_timeout_seconds";
static const kLargeQueueMaxSize = "large_queue_max_size";
final SharedPreferences _prefs;
@@ -217,4 +226,59 @@ class LocalSettings {
bool get hideSharedItemsFromHomeGallery =>
_prefs.getBool(_hideSharedItemsFromHomeGalleryTag) ?? false;
bool get showLocalIDOverThumbnails =>
_prefs.getBool(kShowLocalIDOverThumbnails) ?? false;
Future<void> setShowLocalIDOverThumbnails(bool value) async {
await _prefs.setBool(kShowLocalIDOverThumbnails, value);
}
// Thumbnail queue configuration - Small queue
int get smallQueueMaxConcurrent => _prefs.getInt(kSmallQueueMaxConcurrent) ?? 15;
int get smallQueueTimeoutSeconds => _prefs.getInt(kSmallQueueTimeout) ?? 60;
int get smallQueueMaxSize => _prefs.getInt(kSmallQueueMaxSize) ?? 200;
Future<void> setSmallQueueMaxConcurrent(int value) async {
await _prefs.setInt(kSmallQueueMaxConcurrent, value);
}
Future<void> setSmallQueueTimeout(int seconds) async {
await _prefs.setInt(kSmallQueueTimeout, seconds);
}
Future<void> setSmallQueueMaxSize(int value) async {
await _prefs.setInt(kSmallQueueMaxSize, value);
}
// Thumbnail queue configuration - Large queue
int get largeQueueMaxConcurrent => _prefs.getInt(kLargeQueueMaxConcurrent) ?? 5;
int get largeQueueTimeoutSeconds => _prefs.getInt(kLargeQueueTimeout) ?? 60;
int get largeQueueMaxSize => _prefs.getInt(kLargeQueueMaxSize) ?? 200;
Future<void> setLargeQueueMaxConcurrent(int value) async {
await _prefs.setInt(kLargeQueueMaxConcurrent, value);
}
Future<void> setLargeQueueTimeout(int seconds) async {
await _prefs.setInt(kLargeQueueTimeout, seconds);
}
Future<void> setLargeQueueMaxSize(int value) async {
await _prefs.setInt(kLargeQueueMaxSize, value);
}
// Reset thumbnail queue settings to defaults
Future<void> resetThumbnailQueueSettings() async {
await _prefs.remove(kSmallQueueMaxConcurrent);
await _prefs.remove(kSmallQueueTimeout);
await _prefs.remove(kSmallQueueMaxSize);
await _prefs.remove(kLargeQueueMaxConcurrent);
await _prefs.remove(kLargeQueueTimeout);
await _prefs.remove(kLargeQueueMaxSize);
}
}

View File

@@ -1,3 +1,4 @@
- Ashil: Add new section (show local ID & config local thumb queue) in debug section in settings to help in debugging thumbnail not loading issue.
- Ashil: Revert diskLoadDeferDuration to 80ms (Fixes local thumbnails taking ~1 sec to load on scrolling gallery)
- Ashil: Revert diskLoadDeferDuration to 500ms (Was 80ms before but fixes local thumbnail taking very long to load or never loading)
- Ashil: Revert increase in cache extent for gallery - to check if thumbnail not loading regression resolves