mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-04 09:10:48 +02:00
perf: reduce queue and extension runtime overhead
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user