improved start-up loading time
This commit is contained in:
parent
fc10f97bfa
commit
7aee4c6db3
10 changed files with 50 additions and 62 deletions
|
@ -3,30 +3,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateMetadata {
|
||||
final int? contentId, dateMillis;
|
||||
|
||||
DateMetadata({
|
||||
this.contentId,
|
||||
this.dateMillis,
|
||||
});
|
||||
|
||||
factory DateMetadata.fromMap(Map map) {
|
||||
return DateMetadata(
|
||||
contentId: map['contentId'],
|
||||
dateMillis: map['dateMillis'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'contentId': contentId,
|
||||
'dateMillis': dateMillis,
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{contentId=$contentId, dateMillis=$dateMillis}';
|
||||
}
|
||||
|
||||
class CatalogMetadata {
|
||||
final int? contentId, dateMillis;
|
||||
final bool isAnimated, isGeotiff, is360, isMultiPage;
|
||||
|
|
|
@ -36,7 +36,7 @@ abstract class MetadataDb {
|
|||
|
||||
Future<void> clearDates();
|
||||
|
||||
Future<List<DateMetadata>> loadDates();
|
||||
Future<Map<int?, int?>> loadDates();
|
||||
|
||||
// catalog metadata
|
||||
|
||||
|
@ -260,12 +260,10 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<List<DateMetadata>> loadDates() async {
|
||||
// final stopwatch = Stopwatch()..start();
|
||||
Future<Map<int?, int?>> loadDates() async {
|
||||
final db = await _database;
|
||||
final maps = await db.query(dateTakenTable);
|
||||
final metadataEntries = maps.map((map) => DateMetadata.fromMap(map)).toList();
|
||||
// debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
|
||||
final metadataEntries = Map.fromEntries(maps.map((map) => MapEntry(map['contentId'] as int, (map['dateMillis'] ?? 0) as int)));
|
||||
return metadataEntries;
|
||||
}
|
||||
|
||||
|
@ -280,11 +278,9 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
|
||||
@override
|
||||
Future<List<CatalogMetadata>> loadMetadataEntries() async {
|
||||
// final stopwatch = Stopwatch()..start();
|
||||
final db = await _database;
|
||||
final maps = await db.query(metadataTable);
|
||||
final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList();
|
||||
// debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
|
||||
return metadataEntries;
|
||||
}
|
||||
|
||||
|
@ -318,7 +314,10 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
if (metadata.dateMillis != 0) {
|
||||
batch.insert(
|
||||
dateTakenTable,
|
||||
DateMetadata(contentId: metadata.contentId, dateMillis: metadata.dateMillis).toMap(),
|
||||
{
|
||||
'contentId': metadata.contentId,
|
||||
'dateMillis': metadata.dateMillis,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
@ -340,11 +339,9 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
|
||||
@override
|
||||
Future<List<AddressDetails>> loadAddresses() async {
|
||||
// final stopwatch = Stopwatch()..start();
|
||||
final db = await _database;
|
||||
final maps = await db.query(addressTable);
|
||||
final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList();
|
||||
// debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
|
||||
return addresses;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import 'package:aves/model/filters/album.dart';
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/metadata.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
|
@ -22,6 +21,8 @@ import 'package:flutter/foundation.dart';
|
|||
mixin SourceBase {
|
||||
EventBus get eventBus;
|
||||
|
||||
Map<int?, AvesEntry> get entryById;
|
||||
|
||||
Set<AvesEntry> get visibleEntries;
|
||||
|
||||
List<AvesEntry> get sortedEntriesByDate;
|
||||
|
@ -41,6 +42,11 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
@override
|
||||
EventBus get eventBus => _eventBus;
|
||||
|
||||
final Map<int?, AvesEntry> _entryById = {};
|
||||
|
||||
@override
|
||||
Map<int?, AvesEntry> get entryById => Map.unmodifiable(_entryById);
|
||||
|
||||
final Set<AvesEntry> _rawEntries = {};
|
||||
|
||||
Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries);
|
||||
|
@ -61,11 +67,11 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
return _sortedEntriesByDate!;
|
||||
}
|
||||
|
||||
late List<DateMetadata> _savedDates;
|
||||
late Map<int?, int?> _savedDates;
|
||||
|
||||
Future<void> loadDates() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
_savedDates = List.unmodifiable(await metadataDb.loadDates());
|
||||
_savedDates = Map.unmodifiable(await metadataDb.loadDates());
|
||||
debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${_savedDates.length} entries');
|
||||
}
|
||||
|
||||
|
@ -84,14 +90,16 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
|
||||
void addEntries(Set<AvesEntry> entries) {
|
||||
if (entries.isEmpty) return;
|
||||
|
||||
final newIdMapEntries = Map.fromEntries(entries.map((v) => MapEntry(v.contentId, v)));
|
||||
if (_rawEntries.isNotEmpty) {
|
||||
final newContentIds = entries.map((entry) => entry.contentId).toSet();
|
||||
final newContentIds = newIdMapEntries.keys.toSet();
|
||||
_rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId));
|
||||
}
|
||||
entries.forEach((entry) {
|
||||
final contentId = entry.contentId;
|
||||
entry.catalogDateMillis = _savedDates.firstWhereOrNull((metadata) => metadata.contentId == contentId)?.dateMillis;
|
||||
});
|
||||
|
||||
entries.forEach((entry) => entry.catalogDateMillis = _savedDates[entry.contentId]);
|
||||
|
||||
_entryById.addAll(newIdMapEntries);
|
||||
_rawEntries.addAll(entries);
|
||||
_invalidate(entries);
|
||||
|
||||
|
@ -104,6 +112,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
|
||||
await favourites.remove(entries);
|
||||
await covers.removeEntries(entries);
|
||||
|
||||
entries.forEach((v) => _entryById.remove(v.contentId));
|
||||
_rawEntries.removeAll(entries);
|
||||
_invalidate(entries);
|
||||
|
||||
|
@ -114,6 +124,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
}
|
||||
|
||||
void clearEntries() {
|
||||
_entryById.clear();
|
||||
_rawEntries.clear();
|
||||
_invalidate();
|
||||
|
||||
|
|
|
@ -20,10 +20,8 @@ mixin LocationMixin on SourceBase {
|
|||
Future<void> loadAddresses() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final saved = await metadataDb.loadAddresses();
|
||||
visibleEntries.forEach((entry) {
|
||||
final contentId = entry.contentId;
|
||||
entry.addressDetails = saved.firstWhereOrNull((address) => address.contentId == contentId);
|
||||
});
|
||||
final idMap = entryById;
|
||||
saved.forEach((metadata) => idMap[metadata.contentId]?.addressDetails = metadata);
|
||||
debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries');
|
||||
onAddressMetadataChanged();
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
settings.catalogTimeZone = currentTimeZone;
|
||||
}
|
||||
}
|
||||
await loadDates(); // 100ms for 5400 entries
|
||||
await loadDates();
|
||||
_initialized = true;
|
||||
debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}');
|
||||
}
|
||||
|
@ -49,15 +49,15 @@ class MediaStoreSource extends CollectionSource {
|
|||
stateNotifier.value = SourceState.loading;
|
||||
clearEntries();
|
||||
|
||||
final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries
|
||||
final oldEntries = await metadataDb.loadEntries();
|
||||
final knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId!, entry.dateModifiedSecs!)));
|
||||
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet();
|
||||
oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId));
|
||||
|
||||
// show known entries
|
||||
addEntries(oldEntries);
|
||||
await loadCatalogMetadata(); // 600ms for 5500 entries
|
||||
await loadAddresses(); // 200ms for 3000 entries
|
||||
await loadCatalogMetadata();
|
||||
await loadAddresses();
|
||||
debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}');
|
||||
|
||||
// clean up obsolete entries
|
||||
|
@ -94,7 +94,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
addPendingEntries();
|
||||
debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}');
|
||||
|
||||
await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries
|
||||
await metadataDb.saveEntries(allNewEntries);
|
||||
|
||||
if (allNewEntries.isNotEmpty) {
|
||||
// new entries include existing entries with obsolete paths
|
||||
|
|
|
@ -15,10 +15,8 @@ mixin TagMixin on SourceBase {
|
|||
Future<void> loadCatalogMetadata() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final saved = await metadataDb.loadMetadataEntries();
|
||||
visibleEntries.forEach((entry) {
|
||||
final contentId = entry.contentId;
|
||||
entry.catalogMetadata = saved.firstWhereOrNull((metadata) => metadata.contentId == contentId);
|
||||
});
|
||||
final idMap = entryById;
|
||||
saved.forEach((metadata) => idMap[metadata.contentId]?.catalogMetadata = metadata);
|
||||
debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries');
|
||||
onCatalogMetadataChanged();
|
||||
}
|
||||
|
|
|
@ -95,6 +95,14 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
|||
},
|
||||
title: const Text('Show tasks overlay'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final source = context.read<CollectionSource>();
|
||||
await source.init();
|
||||
await source.refresh();
|
||||
},
|
||||
child: const Text('Source full refresh'),
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
|
|
|
@ -17,7 +17,7 @@ class DebugAppDatabaseSection extends StatefulWidget {
|
|||
class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with AutomaticKeepAliveClientMixin {
|
||||
late Future<int> _dbFileSizeLoader;
|
||||
late Future<Set<AvesEntry>> _dbEntryLoader;
|
||||
late Future<List<DateMetadata>> _dbDateLoader;
|
||||
late Future<Map<int?, int?>> _dbDateLoader;
|
||||
late Future<List<CatalogMetadata>> _dbMetadataLoader;
|
||||
late Future<List<AddressDetails>> _dbAddressLoader;
|
||||
late Future<Set<FavouriteRow>> _dbFavouritesLoader;
|
||||
|
@ -82,7 +82,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
|||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<List>(
|
||||
FutureBuilder<Map<int?, int?>>(
|
||||
future: _dbDateLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||
|
|
|
@ -18,7 +18,7 @@ class DbTab extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DbTabState extends State<DbTab> {
|
||||
late Future<DateMetadata?> _dbDateLoader;
|
||||
late Future<int?> _dbDateLoader;
|
||||
late Future<AvesEntry?> _dbEntryLoader;
|
||||
late Future<CatalogMetadata?> _dbMetadataLoader;
|
||||
late Future<AddressDetails?> _dbAddressLoader;
|
||||
|
@ -33,7 +33,7 @@ class _DbTabState extends State<DbTab> {
|
|||
|
||||
void _loadDatabase() {
|
||||
final contentId = entry.contentId;
|
||||
_dbDateLoader = metadataDb.loadDates().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
|
||||
_dbDateLoader = metadataDb.loadDates().then((values) => values[contentId]);
|
||||
_dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
|
||||
_dbMetadataLoader = metadataDb.loadMetadataEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
|
||||
_dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
|
||||
|
@ -45,7 +45,7 @@ class _DbTabState extends State<DbTab> {
|
|||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
FutureBuilder<DateMetadata?>(
|
||||
FutureBuilder<int?>(
|
||||
future: _dbDateLoader,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return Text(snapshot.error.toString());
|
||||
|
@ -58,7 +58,7 @@ class _DbTabState extends State<DbTab> {
|
|||
if (data != null)
|
||||
InfoRowGroup(
|
||||
info: {
|
||||
'dateMillis': '${data.dateMillis}',
|
||||
'dateMillis': '$data',
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -24,7 +24,7 @@ class FakeMetadataDb extends Fake implements MetadataDb {
|
|||
Future<void> updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(null);
|
||||
|
||||
@override
|
||||
Future<List<DateMetadata>> loadDates() => SynchronousFuture([]);
|
||||
Future<Map<int?, int?>> loadDates() => SynchronousFuture({});
|
||||
|
||||
@override
|
||||
Future<List<CatalogMetadata>> loadMetadataEntries() => SynchronousFuture([]);
|
||||
|
|
Loading…
Reference in a new issue