perf: memory and rebuild optimizations across app

- Bound Deezer cache with LRU eviction and periodic cleanup
- Configure Flutter image cache limits (240 entries / 60 MiB)
- Add ResizeImage wrapper for precacheImage calls
- Add memCacheWidth/cacheWidth to cover images across screens
- Add DownloadedEmbeddedCoverResolver as centralized cover service
- Throttle download progress notifications with dedup checks
- Normalize progress/speed/bytes values to reduce UI rebuilds
- Optimize queue list with per-item ConsumerWidget and RepaintBoundary
- Preserve derived indexes in LocalLibraryState.copyWith
- Skip non-error logs when detailed logging disabled
- Use async file stat and early-break loops in queue filters
This commit is contained in:
zarzet
2026-02-11 01:44:05 +07:00
parent 68e6c8be35
commit a9150b85b9
14 changed files with 891 additions and 140 deletions
@@ -0,0 +1,235 @@
import 'dart:collection';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/file_access.dart';
class _EmbeddedCoverCacheEntry {
final String previewPath;
final int? sourceModTimeMillis;
const _EmbeddedCoverCacheEntry({
required this.previewPath,
this.sourceModTimeMillis,
});
}
/// Shared resolver for embedded cover previews from downloaded/local files.
/// It keeps a bounded in-memory cache and only refreshes extraction
/// when the source file changed.
class DownloadedEmbeddedCoverResolver {
static const int _maxCacheEntries = 160;
static const int _minModCheckIntervalMs = 1200;
static final LinkedHashMap<String, _EmbeddedCoverCacheEntry> _cache =
LinkedHashMap<String, _EmbeddedCoverCacheEntry>();
static final Set<String> _pendingExtract = <String>{};
static final Set<String> _pendingModCheck = <String>{};
static final Set<String> _failedExtract = <String>{};
static final Map<String, int> _lastModCheckMillis = <String, int>{};
static String cleanFilePath(String? filePath) {
if (filePath == null) return '';
if (filePath.startsWith('EXISTS:')) {
return filePath.substring(7);
}
return filePath;
}
static Future<int?> readFileModTimeMillis(String? filePath) async {
final cleanPath = cleanFilePath(filePath);
if (cleanPath.isEmpty) return null;
if (isContentUri(cleanPath)) {
try {
final modTimes = await PlatformBridge.getSafFileModTimes([cleanPath]);
return modTimes[cleanPath];
} catch (_) {
return null;
}
}
try {
final stat = await File(cleanPath).stat();
return stat.modified.millisecondsSinceEpoch;
} catch (_) {
return null;
}
}
static String? resolve(String? filePath, {VoidCallback? onChanged}) {
final cleanPath = cleanFilePath(filePath);
if (cleanPath.isEmpty) return null;
final cached = _cache[cleanPath];
if (cached != null) {
if (File(cached.previewPath).existsSync()) {
_touch(cleanPath, cached);
_scheduleModCheck(cleanPath, onChanged: onChanged);
return cached.previewPath;
}
_cache.remove(cleanPath);
_cleanupTempCoverPathSync(cached.previewPath);
}
_ensureCover(cleanPath, onChanged: onChanged);
return null;
}
static Future<void> scheduleRefreshForPath(
String? filePath, {
int? beforeModTime,
bool force = false,
VoidCallback? onChanged,
}) async {
final cleanPath = cleanFilePath(filePath);
if (cleanPath.isEmpty) return;
if (!force) {
if (beforeModTime == null) return;
final afterModTime = await readFileModTimeMillis(cleanPath);
if (afterModTime != null && afterModTime == beforeModTime) {
return;
}
}
_failedExtract.remove(cleanPath);
_ensureCover(cleanPath, forceRefresh: true, onChanged: onChanged);
}
static void invalidate(String? filePath) {
final cleanPath = cleanFilePath(filePath);
if (cleanPath.isEmpty) return;
final cached = _cache.remove(cleanPath);
_pendingExtract.remove(cleanPath);
_pendingModCheck.remove(cleanPath);
_failedExtract.remove(cleanPath);
_lastModCheckMillis.remove(cleanPath);
if (cached != null) {
_cleanupTempCoverPathSync(cached.previewPath);
}
}
static void _touch(String cleanPath, _EmbeddedCoverCacheEntry entry) {
_cache
..remove(cleanPath)
..[cleanPath] = entry;
}
static void _trimCacheIfNeeded() {
while (_cache.length > _maxCacheEntries) {
final oldestKey = _cache.keys.first;
final removed = _cache.remove(oldestKey);
if (removed != null) {
_cleanupTempCoverPathSync(removed.previewPath);
}
_pendingExtract.remove(oldestKey);
_pendingModCheck.remove(oldestKey);
_failedExtract.remove(oldestKey);
_lastModCheckMillis.remove(oldestKey);
}
}
static void _scheduleModCheck(String cleanPath, {VoidCallback? onChanged}) {
if (_pendingModCheck.contains(cleanPath)) return;
final now = DateTime.now().millisecondsSinceEpoch;
final lastCheck = _lastModCheckMillis[cleanPath] ?? 0;
if (now - lastCheck < _minModCheckIntervalMs) return;
_lastModCheckMillis[cleanPath] = now;
_pendingModCheck.add(cleanPath);
Future.microtask(() async {
try {
final cached = _cache[cleanPath];
if (cached == null) return;
final currentModTime = await readFileModTimeMillis(cleanPath);
if (currentModTime != null &&
cached.sourceModTimeMillis != null &&
currentModTime != cached.sourceModTimeMillis) {
_ensureCover(
cleanPath,
forceRefresh: true,
knownModTime: currentModTime,
onChanged: onChanged,
);
}
} finally {
_pendingModCheck.remove(cleanPath);
}
});
}
static void _ensureCover(
String cleanPath, {
bool forceRefresh = false,
int? knownModTime,
VoidCallback? onChanged,
}) {
if (cleanPath.isEmpty) return;
if (_pendingExtract.contains(cleanPath)) return;
if (!forceRefresh && _cache.containsKey(cleanPath)) return;
if (!forceRefresh && _failedExtract.contains(cleanPath)) return;
_pendingExtract.add(cleanPath);
Future.microtask(() async {
String? outputPath;
try {
final modTime = knownModTime ?? await readFileModTimeMillis(cleanPath);
final tempDir = await Directory.systemTemp.createTemp(
'download_cover_preview_',
);
outputPath =
'${tempDir.path}${Platform.pathSeparator}cover_preview.jpg';
final result = await PlatformBridge.extractCoverToFile(
cleanPath,
outputPath,
);
final hasCover =
result['error'] == null && await File(outputPath).exists();
if (!hasCover) {
_failedExtract.add(cleanPath);
_cleanupTempCoverPathSync(outputPath);
return;
}
final previous = _cache[cleanPath];
final next = _EmbeddedCoverCacheEntry(
previewPath: outputPath,
sourceModTimeMillis: modTime,
);
_touch(cleanPath, next);
_failedExtract.remove(cleanPath);
_trimCacheIfNeeded();
if (previous != null && previous.previewPath != outputPath) {
_cleanupTempCoverPathSync(previous.previewPath);
}
onChanged?.call();
} catch (_) {
_failedExtract.add(cleanPath);
_cleanupTempCoverPathSync(outputPath);
} finally {
_pendingExtract.remove(cleanPath);
}
});
}
static void _cleanupTempCoverPathSync(String? coverPath) {
if (coverPath == null || coverPath.isEmpty) return;
try {
final file = File(coverPath);
if (file.existsSync()) {
file.deleteSync();
}
final parent = file.parent;
if (parent.existsSync()) {
parent.deleteSync(recursive: true);
}
} catch (_) {}
}
}