mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
perf(history): coalesce index bumps and batch per-row exists lookups
This commit is contained in:
@@ -363,9 +363,13 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
bool _isAudioMetadataBackfillInProgress = false;
|
||||
bool _startupMaintenanceScheduled = false;
|
||||
Future<void> _historyWriteChain = Future<void>.value();
|
||||
Timer? _indexBumpTimer;
|
||||
DateTime? _lastIndexBumpAt;
|
||||
static const _indexBumpWindow = Duration(seconds: 1);
|
||||
|
||||
@override
|
||||
DownloadHistoryState build() {
|
||||
ref.onDispose(() => _indexBumpTimer?.cancel());
|
||||
_loadFromDatabaseSync();
|
||||
return DownloadHistoryState();
|
||||
}
|
||||
@@ -1162,22 +1166,49 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
await _db.upsert(resolved.item.toJson());
|
||||
_putResolvedHistoryInMemory(resolved.item, resolved.existingId);
|
||||
}
|
||||
int? persistedCount;
|
||||
try {
|
||||
persistedCount = await _db.getCount();
|
||||
} catch (error) {
|
||||
_historyLog.w('History saved but count refresh failed: $error');
|
||||
}
|
||||
state = state.copyWith(
|
||||
totalCount: persistedCount ?? state.totalCount,
|
||||
loadedIndexVersion: state.loadedIndexVersion + 1,
|
||||
);
|
||||
_scheduleIndexBump();
|
||||
} catch (e, stack) {
|
||||
_historyLog.e('Failed to $action: $e', e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Coalesces the post-persist count refresh + index bump to at most one per
|
||||
/// [_indexBumpWindow]. Every bump invalidates the DB-derived views (queue
|
||||
/// union queries, grouped counts, per-row exists checks) across all
|
||||
/// keep-alive tabs, so per-item bumps during a batch fan out into repeated
|
||||
/// full-table work.
|
||||
void _scheduleIndexBump() {
|
||||
if (_indexBumpTimer != null) return;
|
||||
final now = DateTime.now();
|
||||
final sinceLast = _lastIndexBumpAt == null
|
||||
? _indexBumpWindow
|
||||
: now.difference(_lastIndexBumpAt!);
|
||||
if (sinceLast >= _indexBumpWindow) {
|
||||
_lastIndexBumpAt = now;
|
||||
unawaited(_bumpIndexNow());
|
||||
return;
|
||||
}
|
||||
_indexBumpTimer = Timer(_indexBumpWindow - sinceLast, () {
|
||||
_indexBumpTimer = null;
|
||||
_lastIndexBumpAt = DateTime.now();
|
||||
unawaited(_bumpIndexNow());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _bumpIndexNow() async {
|
||||
int? persistedCount;
|
||||
try {
|
||||
persistedCount = await _db.getCount();
|
||||
} catch (error) {
|
||||
_historyLog.w('History saved but count refresh failed: $error');
|
||||
}
|
||||
state = state.copyWith(
|
||||
totalCount: persistedCount ?? state.totalCount,
|
||||
loadedIndexVersion: state.loadedIndexVersion + 1,
|
||||
);
|
||||
}
|
||||
|
||||
DownloadHistoryItem _putInMemoryTrackVariant(DownloadHistoryItem item) {
|
||||
final isReplacement = state.items.any((existing) => existing.id == item.id);
|
||||
final items = [
|
||||
|
||||
@@ -1031,14 +1031,16 @@ final localLibraryFirstCoverProvider = FutureProvider.autoDispose
|
||||
ref.watch(
|
||||
localLibraryProvider.select((state) => state.loadedIndexVersion),
|
||||
);
|
||||
for (final track in request.tracks) {
|
||||
final cover = _nonEmptyCoverPath(
|
||||
await LibraryDatabase.instance.findExisting(
|
||||
final rows = await LibraryDatabase.instance.findExistingBatch([
|
||||
for (final track in request.tracks)
|
||||
LocalLibraryBatchLookupRequest(
|
||||
isrc: track.isrc,
|
||||
trackName: track.trackName,
|
||||
artistName: track.artistName,
|
||||
),
|
||||
);
|
||||
]);
|
||||
for (final row in rows) {
|
||||
final cover = _nonEmptyCoverPath(row);
|
||||
if (cover != null) return cover;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -2735,6 +2735,16 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
|
||||
if (sortedTracks.isNotEmpty) {
|
||||
final historyLookups = sortedTracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
final existingHistoryKeys = ref
|
||||
.watch(
|
||||
downloadHistoryBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
),
|
||||
)
|
||||
.maybeWhen(data: (keys) => keys, orElse: () => const <String>{});
|
||||
slivers.addAll(
|
||||
_buildVirtualizedResultSection(
|
||||
title: context.l10n.searchSongs,
|
||||
@@ -2753,6 +2763,9 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
searchExtensionId: searchExtensionId,
|
||||
showLocalLibraryIndicator: showLocalLibraryIndicator,
|
||||
thumbnailSizesByExtensionId: thumbnailSizesByExtensionId,
|
||||
isInHistory: existingHistoryKeys.contains(
|
||||
historyLookups[index].lookupKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -197,6 +197,10 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
final bool showLocalLibraryIndicator;
|
||||
final Map<String, (double, double)> thumbnailSizesByExtensionId;
|
||||
|
||||
/// Resolved by the result page via one batch lookup instead of a per-row
|
||||
/// exists query (which in SAF mode also costs a bridge call per row).
|
||||
final bool isInHistory;
|
||||
|
||||
const _TrackItemWithStatus({
|
||||
super.key,
|
||||
required this.track,
|
||||
@@ -206,6 +210,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
required this.searchExtensionId,
|
||||
required this.showLocalLibraryIndicator,
|
||||
required this.thumbnailSizesByExtensionId,
|
||||
required this.isInHistory,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -218,11 +223,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
final historyLookup = historyLookupForTrack(track);
|
||||
final isInHistory = ref
|
||||
.watch(downloadHistoryExistsProvider(historyLookup))
|
||||
.maybeWhen(data: (exists) => exists, orElse: () => false);
|
||||
|
||||
final isInLocalLibrary = showLocalLibraryIndicator
|
||||
? ref.watch(
|
||||
localLibraryProvider.select(
|
||||
|
||||
@@ -626,11 +626,13 @@ class HistoryDatabase {
|
||||
String trackName,
|
||||
String artistName,
|
||||
) async {
|
||||
final key = matchKeyFor(trackName, artistName);
|
||||
if (key.isEmpty) return null;
|
||||
final db = await database;
|
||||
final rows = await db.query(
|
||||
'history',
|
||||
where: 'LOWER(track_name) = ? AND LOWER(artist_name) = ?',
|
||||
whereArgs: [trackName.toLowerCase(), artistName.toLowerCase()],
|
||||
where: 'match_key = ?',
|
||||
whereArgs: [key],
|
||||
orderBy: 'downloaded_at DESC',
|
||||
limit: 1,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user