4ede37d9ad
The prefetcher + alwaysRefresh combination was creating a feedback loop visible in the logs as repeated `metadataPrefetcher: warming N albums` cycles, each kicking N parallel getAlbum fetches that then triggered drift writes that triggered re-emits that re-ran the prefetcher. Tap-to-play was queueing behind 14+ in-flight cache fetches. Three structural fixes: 1. Prefetcher hard-dedupes per session via _warmedArtists Set. Re-rendering a screen no longer re-fires fetches for ids we've already seen. 2. Prefetcher only warms artistProvider, not albumProvider. Albums carry track lists; pre-warming N albums fans out N parallel "fetch tracks" round trips for content the user may never visit. Artist rows are single-row lookups — cheap. Album detail loads on tap (still fast: server-side perf work makes it ~one round trip). 3. Drop alwaysRefresh from albumProvider, artistProvider, artistAlbumsProvider, artistTracksProvider. Each was kicking one silent background refresh per first cache hit. With the prefetcher creating many subscriptions in parallel, that meant every prewarmed id triggered an extra fetch even when drift was already populated. playlistsListProvider keeps alwaysRefresh — system playlists genuinely rotate UUIDs and need the catch-up. Pull-to- refresh remains the explicit invalidation path everywhere else. Removed the warmAlbums calls from the library Albums tab and artist detail album grid (the storm sources). Net effect: cold app boot warms ~12-15 artist rows once, period. Tapping a tile still fetches its detail on demand (one round trip, fast). User-initiated playback isn't queued behind cache work.
77 lines
2.7 KiB
Dart
77 lines
2.7 KiB
Dart
import 'package:flutter/foundation.dart' show debugPrint;
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../library/library_providers.dart';
|
|
import '../models/home_data.dart';
|
|
|
|
/// Pre-warms the drift cache for likely-tap targets. Conservative on
|
|
/// purpose: only warms artistProvider rows (single row, single round
|
|
/// trip per id) and only ever fires once per id per session. Album
|
|
/// detail is NOT prewarmed — the albumProvider auto-fetches its track
|
|
/// list when missing, and pre-warming N albums fans out N parallel
|
|
/// "fetch tracks" round trips that compete with the user's actual
|
|
/// playback request for bandwidth.
|
|
class MetadataPrefetcher {
|
|
MetadataPrefetcher(this._ref) {
|
|
_ref.listen<AsyncValue<HomeData>>(homeProvider, (_, next) {
|
|
next.whenData(_warmHome);
|
|
});
|
|
}
|
|
|
|
final Ref _ref;
|
|
|
|
/// Per-session dedupe so re-rendering a screen (every UI rebuild
|
|
/// fires the data: callback) doesn't trigger N more fetches for
|
|
/// ids we've already warmed.
|
|
final Set<String> _warmedArtists = {};
|
|
|
|
/// Cap per section. Covers what fits on screen without scrolling.
|
|
static const _topN = 8;
|
|
|
|
/// Pre-warm artists. Albums are intentionally not pre-warmed —
|
|
/// see class comment.
|
|
void warmArtists(Iterable<String> ids) {
|
|
var n = 0;
|
|
for (final id in ids) {
|
|
if (id.isEmpty) continue;
|
|
if (!_warmedArtists.add(id)) continue; // already warmed
|
|
if (n++ >= _topN) break;
|
|
_swallow(_ref.read(artistProvider(id).future));
|
|
}
|
|
if (n > 0) debugPrint('metadataPrefetcher: warming $n artists');
|
|
}
|
|
|
|
void _warmHome(HomeData h) {
|
|
final artistIds = <String>{};
|
|
for (final a in h.recentlyAddedAlbums.take(_topN)) {
|
|
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
|
|
}
|
|
for (final a in h.rediscoverAlbums.take(_topN)) {
|
|
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
|
|
}
|
|
for (final ar in h.rediscoverArtists.take(_topN)) {
|
|
if (ar.id.isNotEmpty) artistIds.add(ar.id);
|
|
}
|
|
for (final ar in h.lastPlayedArtists.take(_topN)) {
|
|
if (ar.id.isNotEmpty) artistIds.add(ar.id);
|
|
}
|
|
for (final t in h.mostPlayedTracks.take(_topN)) {
|
|
if (t.artistId.isNotEmpty) artistIds.add(t.artistId);
|
|
}
|
|
warmArtists(artistIds);
|
|
}
|
|
|
|
/// Discards the return value and any error from a fire-and-forget
|
|
/// provider read. We don't care about the value here — we only want
|
|
/// the side effect of writing drift.
|
|
void _swallow(Future<Object?> f) {
|
|
f.then<void>((_) {}).onError((_, __) {});
|
|
}
|
|
}
|
|
|
|
/// Read once at app start to activate the prefetcher (e.g. wire it
|
|
/// from a top-level Consumer or main.dart container override).
|
|
final metadataPrefetcherProvider = Provider<MetadataPrefetcher>((ref) {
|
|
return MetadataPrefetcher(ref);
|
|
});
|