From cbee637501800f6bf4aa16c405f002c332adac8e Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 26 Jul 2026 22:25:04 +0700 Subject: [PATCH] perf(history): coalesce index bumps and batch per-row exists lookups --- lib/providers/download_history_provider.dart | 51 ++++++++++++++++---- lib/providers/local_library_provider.dart | 10 ++-- lib/screens/home_tab.dart | 13 +++++ lib/screens/home_tab_widgets.dart | 10 ++-- lib/services/history_database.dart | 6 ++- 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/lib/providers/download_history_provider.dart b/lib/providers/download_history_provider.dart index bbe16229..c5203b75 100644 --- a/lib/providers/download_history_provider.dart +++ b/lib/providers/download_history_provider.dart @@ -363,9 +363,13 @@ class DownloadHistoryNotifier extends Notifier { bool _isAudioMetadataBackfillInProgress = false; bool _startupMaintenanceScheduled = false; Future _historyWriteChain = Future.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 { 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 _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 = [ diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index ab66ba4f..333009bb 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -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; diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index aacacc54..57d847f6 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -2735,6 +2735,16 @@ class _HomeTabState extends ConsumerState } if (sortedTracks.isNotEmpty) { + final historyLookups = sortedTracks + .map(historyLookupForTrack) + .toList(growable: false); + final existingHistoryKeys = ref + .watch( + downloadHistoryBatchExistsProvider( + HistoryBatchLookupRequest(historyLookups), + ), + ) + .maybeWhen(data: (keys) => keys, orElse: () => const {}); slivers.addAll( _buildVirtualizedResultSection( title: context.l10n.searchSongs, @@ -2753,6 +2763,9 @@ class _HomeTabState extends ConsumerState searchExtensionId: searchExtensionId, showLocalLibraryIndicator: showLocalLibraryIndicator, thumbnailSizesByExtensionId: thumbnailSizesByExtensionId, + isInHistory: existingHistoryKeys.contains( + historyLookups[index].lookupKey, + ), ), ), ); diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index acfc7462..88e4874d 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -197,6 +197,10 @@ class _TrackItemWithStatus extends ConsumerWidget { final bool showLocalLibraryIndicator; final Map 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( diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index abfd8b89..99af6246 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -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, );