perf: reduce queue and extension runtime overhead

This commit is contained in:
zarzet
2026-08-28 01:47:51 +07:00
parent a744514e10
commit 433f8ed685
16 changed files with 251 additions and 90 deletions
+19 -9
View File
@@ -1861,27 +1861,37 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
continue;
}
final maxConcurrent = ref
.read(settingsProvider)
.concurrentDownloads
.clamp(1, 3);
if (activeDownloads.length >= maxConcurrent) {
// Keep pause/settings changes responsive without rescanning the full
// queue while every worker slot is already occupied.
await Future.any([
Future.any(activeDownloads.values),
Future<void>.delayed(_queueSchedulingInterval),
]);
continue;
}
final availableSlots = maxConcurrent - activeDownloads.length;
final queuedItems = state.items
.where(
(item) =>
item.status == DownloadStatus.queued &&
!_pausePendingItemIds.contains(item.id),
)
.toList();
.take(availableSlots)
.toList(growable: false);
if (queuedItems.isEmpty && activeDownloads.isEmpty) {
_log.d('No more items to process');
break;
}
final maxConcurrent = ref
.read(settingsProvider)
.concurrentDownloads
.clamp(1, 3);
while (activeDownloads.length < maxConcurrent &&
queuedItems.isNotEmpty &&
!state.isPaused) {
final item = queuedItems.removeAt(0);
for (final item in queuedItems) {
if (state.isPaused) break;
updateItemStatus(item.id, DownloadStatus.downloading);
+20
View File
@@ -80,6 +80,7 @@ class DownloadQueueLookup {
final Map<String, DownloadItem> byItemId;
final Map<String, int> indexByItemId;
final List<String> itemIds;
final List<String> notCompletedItemIds;
final int queuedCount;
final int completedCount;
final int failedCount;
@@ -91,6 +92,7 @@ class DownloadQueueLookup {
byItemId = const {},
indexByItemId = const {},
itemIds = const [],
notCompletedItemIds = const [],
queuedCount = 0,
completedCount = 0,
failedCount = 0,
@@ -102,6 +104,7 @@ class DownloadQueueLookup {
required this.byItemId,
required this.indexByItemId,
required this.itemIds,
required this.notCompletedItemIds,
required this.queuedCount,
required this.completedCount,
required this.failedCount,
@@ -114,6 +117,7 @@ class DownloadQueueLookup {
final byItemId = <String, DownloadItem>{};
final indexByItemId = <String, int>{};
final itemIds = <String>[];
final notCompletedItemIds = <String>[];
var queuedCount = 0;
var completedCount = 0;
var failedCount = 0;
@@ -125,6 +129,9 @@ class DownloadQueueLookup {
byItemId[item.id] = item;
indexByItemId[item.id] = index;
itemIds.add(item.id);
if (item.status != DownloadStatus.completed) {
notCompletedItemIds.add(item.id);
}
if (_countsAsQueued(item.status)) queuedCount++;
if (item.status == DownloadStatus.completed) completedCount++;
if (item.status == DownloadStatus.failed) failedCount++;
@@ -136,6 +143,7 @@ class DownloadQueueLookup {
byItemId: Map.unmodifiable(byItemId),
indexByItemId: Map.unmodifiable(indexByItemId),
itemIds: List.unmodifiable(itemIds),
notCompletedItemIds: List.unmodifiable(notCompletedItemIds),
queuedCount: queuedCount,
completedCount: completedCount,
failedCount: failedCount,
@@ -185,6 +193,7 @@ class DownloadQueueLookup {
var nextFailedCount = failedCount;
var nextActiveDownloadsCount = activeDownloadsCount;
var nextFinalizingCount = finalizingCount;
var notCompletedMembershipChanged = false;
Map<String, DownloadItem>? nextByItemId;
Map<String, DownloadItem>? nextByTrackId;
@@ -195,6 +204,11 @@ class DownloadQueueLookup {
return DownloadQueueLookup.fromItems(nextItems);
}
if ((previous.status == DownloadStatus.completed) !=
(next.status == DownloadStatus.completed)) {
notCompletedMembershipChanged = true;
}
nextByItemId ??= Map<String, DownloadItem>.from(byItemId);
nextByItemId[next.id] = next;
if (byTrackId[next.track.id]?.id == previous.id) {
@@ -237,6 +251,12 @@ class DownloadQueueLookup {
: Map.unmodifiable(nextByItemId),
indexByItemId: indexByItemId,
itemIds: itemIds,
notCompletedItemIds: notCompletedMembershipChanged
? List.unmodifiable([
for (final item in nextItems)
if (item.status != DownloadStatus.completed) item.id,
])
: notCompletedItemIds,
queuedCount: nextQueuedCount,
completedCount: nextCompletedCount,
failedCount: nextFailedCount,
+25 -14
View File
@@ -1314,6 +1314,7 @@ class _SyncedLyricsViewState extends ConsumerState<_SyncedLyricsView> {
ProviderSubscription<bool>? _playingSubscription;
ProviderSubscription<bool>? _loadingSubscription;
Timer? _lineBoundaryTimer;
Timer? _userScrollIdleTimer;
late List<GlobalKey> _lineKeys;
int _active = -1;
Duration _activeTransitionPosition = Duration.zero;
@@ -1432,6 +1433,7 @@ class _SyncedLyricsViewState extends ConsumerState<_SyncedLyricsView> {
_playingSubscription?.close();
_loadingSubscription?.close();
_lineBoundaryTimer?.cancel();
_userScrollIdleTimer?.cancel();
_scroll.dispose();
super.dispose();
}
@@ -1488,7 +1490,8 @@ class _SyncedLyricsViewState extends ConsumerState<_SyncedLyricsView> {
onNotification: (notification) {
if (notification.direction != ScrollDirection.idle) {
_userScrolling = true;
Future.delayed(const Duration(seconds: 4), () {
_userScrollIdleTimer?.cancel();
_userScrollIdleTimer = Timer(const Duration(seconds: 4), () {
if (mounted) _userScrolling = false;
});
}
@@ -1883,13 +1886,27 @@ class _SweepingTimedLyricTextState extends State<_SweepingTimedLyricText> {
? constraints.maxWidth
: pendingPainter.width;
final height = pendingPainter.height;
final segmentBoxes = <List<TextBox>>[];
var segmentOffset = 0;
for (final segment in widget.segments) {
final segmentEnd = segmentOffset + segment.length;
segmentBoxes.add(
highlightedPainter.getBoxesForSelection(
TextSelection(
baseOffset: segmentOffset,
extentOffset: segmentEnd,
),
),
);
segmentOffset = segmentEnd;
}
return Semantics(
label: widget.semanticsLabel,
child: CustomPaint(
size: Size(width, height),
painter: _TimedLyricSweepPainter(
segments: widget.segments,
segmentBoxes: segmentBoxes,
starts: widget.starts,
ends: widget.ends,
currentPosition: widget.currentPosition,
@@ -1905,7 +1922,7 @@ class _SweepingTimedLyricTextState extends State<_SweepingTimedLyricText> {
}
class _TimedLyricSweepPainter extends CustomPainter {
final List<String> segments;
final List<List<TextBox>> segmentBoxes;
final List<Duration> starts;
final List<Duration> ends;
final Duration Function() currentPosition;
@@ -1913,7 +1930,7 @@ class _TimedLyricSweepPainter extends CustomPainter {
final TextPainter highlightedPainter;
_TimedLyricSweepPainter({
required this.segments,
required this.segmentBoxes,
required this.starts,
required this.ends,
required this.currentPosition,
@@ -1929,9 +1946,7 @@ class _TimedLyricSweepPainter extends CustomPainter {
final completedPath = Path();
final partialBoxes = <(Rect, double)>[];
final position = currentPosition();
var offset = 0;
for (var index = 0; index < segments.length; index++) {
final end = offset + segments[index].length;
for (var index = 0; index < segmentBoxes.length; index++) {
final value = index < starts.length && index < ends.length
? syncedLyricSegmentProgress(
position: position,
@@ -1939,11 +1954,8 @@ class _TimedLyricSweepPainter extends CustomPainter {
end: ends[index],
)
: 0.0;
if (value > 0 && end > offset) {
final boxes = highlightedPainter.getBoxesForSelection(
TextSelection(baseOffset: offset, extentOffset: end),
);
for (final box in boxes) {
if (value > 0) {
for (final box in segmentBoxes[index]) {
final rect = box.toRect();
if (value >= 1) {
completedPath.addRect(rect);
@@ -1952,7 +1964,6 @@ class _TimedLyricSweepPainter extends CustomPainter {
}
}
}
offset = end;
}
if (!completedPath.getBounds().isEmpty) {
@@ -1998,7 +2009,7 @@ class _TimedLyricSweepPainter extends CustomPainter {
@override
bool shouldRepaint(covariant _TimedLyricSweepPainter oldDelegate) {
return oldDelegate.segments != segments ||
return oldDelegate.segmentBoxes != segmentBoxes ||
oldDelegate.starts != starts ||
oldDelegate.ends != ends ||
oldDelegate.currentPosition != currentPosition ||
+8 -12
View File
@@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/material.dart';
import 'package:spotiflac_android/services/shell_navigation_service.dart';
import 'package:spotiflac_android/widgets/error_card.dart';
@@ -1223,7 +1223,10 @@ class _QueueTabState extends ConsumerState<QueueTab> {
ref.listen(downloadQueueLookupProvider, (previous, next) {
if (previous == null) return;
for (final id in previous.itemIds) {
if (identical(previous.notCompletedItemIds, next.notCompletedItemIds)) {
return;
}
for (final id in previous.notCompletedItemIds) {
final prevItem = previous.byItemId[id];
final nextItem = next.byItemId[id];
if (prevItem == null) continue;
@@ -1656,16 +1659,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
return Consumer(
builder: (context, ref, child) {
final queueCount = ref.watch(
downloadQueueLookupProvider.select((lookup) {
var count = 0;
for (final id in lookup.itemIds) {
final entry = lookup.byItemId[id];
if (entry != null && entry.status != DownloadStatus.completed) {
count++;
}
}
return count;
}),
downloadQueueLookupProvider.select(
(lookup) => lookup.notCompletedItemIds.length,
),
);
final failedCount = ref.watch(
downloadQueueProvider.select((state) => state.failedCount),
+3 -12
View File
@@ -57,19 +57,10 @@ extension _QueueTabFilterWidgets on _QueueTabState {
? const <String>[]
: ref
.watch(
downloadQueueLookupProvider.select((lookup) {
final ids = <String>[];
for (final id in lookup.itemIds) {
final entry = lookup.byItemId[id];
if (entry != null &&
entry.status != DownloadStatus.completed) {
ids.add(id);
}
}
return _QueueItemIdsSnapshot(ids);
}),
downloadQueueLookupProvider.select(
(lookup) => lookup.notCompletedItemIds,
),
)
.ids
.reversed
.toList(growable: false);
-14
View File
@@ -495,17 +495,3 @@ final _queueLibraryCountsProvider = FutureProvider.autoDispose
);
return LibraryDatabase.instance.getQueueCounts(request.toDbQuery());
});
class _QueueItemIdsSnapshot {
final List<String> ids;
const _QueueItemIdsSnapshot(this.ids);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _QueueItemIdsSnapshot && listEquals(ids, other.ids);
@override
int get hashCode => Object.hashAll(ids);
}
+23 -9
View File
@@ -36,6 +36,24 @@ void setPlaybackNormalizationEnabled(bool enabled) {
_activeMusicPlayerHandler?.reapplyNormalization();
}
List<int> buildShuffleCandidatePool({
required int mediaCount,
required int currentIndex,
required Iterable<int> recentIndices,
}) {
final recent = recentIndices.toSet();
final pool = <int>[];
for (var index = 0; index < mediaCount; index++) {
if (index != currentIndex && !recent.contains(index)) pool.add(index);
}
if (pool.isEmpty) {
for (var index = 0; index < mediaCount; index++) {
if (index != currentIndex) pool.add(index);
}
}
return pool;
}
final AudioContext _musicAudioContext = AudioContext(
android: const AudioContextAndroid(
audioFocus: AndroidAudioFocus.none,
@@ -967,15 +985,11 @@ class MusicPlayerHandler extends BaseAudioHandler
int _pickNextShuffle() {
if (_media.length <= 1) return _index;
final pool = <int>[];
for (var i = 0; i < _media.length; i++) {
if (i != _index && !_recent.contains(i)) pool.add(i);
}
if (pool.isEmpty) {
for (var i = 0; i < _media.length; i++) {
if (i != _index) pool.add(i);
}
}
final pool = buildShuffleCandidatePool(
mediaCount: _media.length,
currentIndex: _index,
recentIndices: _recent,
);
return pool[_random.nextInt(pool.length)];
}