perf(history): coalesce index bumps and batch per-row exists lookups

This commit is contained in:
zarzet
2026-07-26 22:25:04 +07:00
parent 1cda88cafc
commit cbee637501
5 changed files with 69 additions and 21 deletions
+41 -10
View File
@@ -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 = [
+6 -4
View File
@@ -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;