improved start-up loading time

This commit is contained in:
Thibault Deckers 2021-08-24 19:08:25 +09:00
parent fc10f97bfa
commit 7aee4c6db3
10 changed files with 50 additions and 62 deletions

View file

@ -3,30 +3,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:intl/intl.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 { class CatalogMetadata {
final int? contentId, dateMillis; final int? contentId, dateMillis;
final bool isAnimated, isGeotiff, is360, isMultiPage; final bool isAnimated, isGeotiff, is360, isMultiPage;

View file

@ -36,7 +36,7 @@ abstract class MetadataDb {
Future<void> clearDates(); Future<void> clearDates();
Future<List<DateMetadata>> loadDates(); Future<Map<int?, int?>> loadDates();
// catalog metadata // catalog metadata
@ -260,12 +260,10 @@ class SqfliteMetadataDb implements MetadataDb {
} }
@override @override
Future<List<DateMetadata>> loadDates() async { Future<Map<int?, int?>> loadDates() async {
// final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final maps = await db.query(dateTakenTable); final maps = await db.query(dateTakenTable);
final metadataEntries = maps.map((map) => DateMetadata.fromMap(map)).toList(); final metadataEntries = Map.fromEntries(maps.map((map) => MapEntry(map['contentId'] as int, (map['dateMillis'] ?? 0) as int)));
// debugPrint('$runtimeType loadDates complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
return metadataEntries; return metadataEntries;
} }
@ -280,11 +278,9 @@ class SqfliteMetadataDb implements MetadataDb {
@override @override
Future<List<CatalogMetadata>> loadMetadataEntries() async { Future<List<CatalogMetadata>> loadMetadataEntries() async {
// final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final maps = await db.query(metadataTable); final maps = await db.query(metadataTable);
final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList(); final metadataEntries = maps.map((map) => CatalogMetadata.fromMap(map)).toList();
// debugPrint('$runtimeType loadMetadataEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
return metadataEntries; return metadataEntries;
} }
@ -318,7 +314,10 @@ class SqfliteMetadataDb implements MetadataDb {
if (metadata.dateMillis != 0) { if (metadata.dateMillis != 0) {
batch.insert( batch.insert(
dateTakenTable, dateTakenTable,
DateMetadata(contentId: metadata.contentId, dateMillis: metadata.dateMillis).toMap(), {
'contentId': metadata.contentId,
'dateMillis': metadata.dateMillis,
},
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
} }
@ -340,11 +339,9 @@ class SqfliteMetadataDb implements MetadataDb {
@override @override
Future<List<AddressDetails>> loadAddresses() async { Future<List<AddressDetails>> loadAddresses() async {
// final stopwatch = Stopwatch()..start();
final db = await _database; final db = await _database;
final maps = await db.query(addressTable); final maps = await db.query(addressTable);
final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList(); final addresses = maps.map((map) => AddressDetails.fromMap(map)).toList();
// debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
return addresses; return addresses;
} }

View file

@ -7,7 +7,6 @@ import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/tag.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/settings/settings.dart';
import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
@ -22,6 +21,8 @@ import 'package:flutter/foundation.dart';
mixin SourceBase { mixin SourceBase {
EventBus get eventBus; EventBus get eventBus;
Map<int?, AvesEntry> get entryById;
Set<AvesEntry> get visibleEntries; Set<AvesEntry> get visibleEntries;
List<AvesEntry> get sortedEntriesByDate; List<AvesEntry> get sortedEntriesByDate;
@ -41,6 +42,11 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
@override @override
EventBus get eventBus => _eventBus; EventBus get eventBus => _eventBus;
final Map<int?, AvesEntry> _entryById = {};
@override
Map<int?, AvesEntry> get entryById => Map.unmodifiable(_entryById);
final Set<AvesEntry> _rawEntries = {}; final Set<AvesEntry> _rawEntries = {};
Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries); Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries);
@ -61,11 +67,11 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
return _sortedEntriesByDate!; return _sortedEntriesByDate!;
} }
late List<DateMetadata> _savedDates; late Map<int?, int?> _savedDates;
Future<void> loadDates() async { Future<void> loadDates() async {
final stopwatch = Stopwatch()..start(); 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'); 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) { void addEntries(Set<AvesEntry> entries) {
if (entries.isEmpty) return; if (entries.isEmpty) return;
final newIdMapEntries = Map.fromEntries(entries.map((v) => MapEntry(v.contentId, v)));
if (_rawEntries.isNotEmpty) { if (_rawEntries.isNotEmpty) {
final newContentIds = entries.map((entry) => entry.contentId).toSet(); final newContentIds = newIdMapEntries.keys.toSet();
_rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId)); _rawEntries.removeWhere((entry) => newContentIds.contains(entry.contentId));
} }
entries.forEach((entry) {
final contentId = entry.contentId; entries.forEach((entry) => entry.catalogDateMillis = _savedDates[entry.contentId]);
entry.catalogDateMillis = _savedDates.firstWhereOrNull((metadata) => metadata.contentId == contentId)?.dateMillis;
}); _entryById.addAll(newIdMapEntries);
_rawEntries.addAll(entries); _rawEntries.addAll(entries);
_invalidate(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(); final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
await favourites.remove(entries); await favourites.remove(entries);
await covers.removeEntries(entries); await covers.removeEntries(entries);
entries.forEach((v) => _entryById.remove(v.contentId));
_rawEntries.removeAll(entries); _rawEntries.removeAll(entries);
_invalidate(entries); _invalidate(entries);
@ -114,6 +124,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
} }
void clearEntries() { void clearEntries() {
_entryById.clear();
_rawEntries.clear(); _rawEntries.clear();
_invalidate(); _invalidate();

View file

@ -20,10 +20,8 @@ mixin LocationMixin on SourceBase {
Future<void> loadAddresses() async { Future<void> loadAddresses() async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final saved = await metadataDb.loadAddresses(); final saved = await metadataDb.loadAddresses();
visibleEntries.forEach((entry) { final idMap = entryById;
final contentId = entry.contentId; saved.forEach((metadata) => idMap[metadata.contentId]?.addressDetails = metadata);
entry.addressDetails = saved.firstWhereOrNull((address) => address.contentId == contentId);
});
debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries'); debugPrint('$runtimeType loadAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries');
onAddressMetadataChanged(); onAddressMetadataChanged();
} }

View file

@ -36,7 +36,7 @@ class MediaStoreSource extends CollectionSource {
settings.catalogTimeZone = currentTimeZone; settings.catalogTimeZone = currentTimeZone;
} }
} }
await loadDates(); // 100ms for 5400 entries await loadDates();
_initialized = true; _initialized = true;
debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}'); debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}');
} }
@ -49,15 +49,15 @@ class MediaStoreSource extends CollectionSource {
stateNotifier.value = SourceState.loading; stateNotifier.value = SourceState.loading;
clearEntries(); 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 knownDateById = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId!, entry.dateModifiedSecs!)));
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet(); final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(knownDateById.keys.toList())).toSet();
oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId)); oldEntries.removeWhere((entry) => obsoleteContentIds.contains(entry.contentId));
// show known entries // show known entries
addEntries(oldEntries); addEntries(oldEntries);
await loadCatalogMetadata(); // 600ms for 5500 entries await loadCatalogMetadata();
await loadAddresses(); // 200ms for 3000 entries await loadAddresses();
debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}'); debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}');
// clean up obsolete entries // clean up obsolete entries
@ -94,7 +94,7 @@ class MediaStoreSource extends CollectionSource {
addPendingEntries(); addPendingEntries();
debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}'); 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) { if (allNewEntries.isNotEmpty) {
// new entries include existing entries with obsolete paths // new entries include existing entries with obsolete paths

View file

@ -15,10 +15,8 @@ mixin TagMixin on SourceBase {
Future<void> loadCatalogMetadata() async { Future<void> loadCatalogMetadata() async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final saved = await metadataDb.loadMetadataEntries(); final saved = await metadataDb.loadMetadataEntries();
visibleEntries.forEach((entry) { final idMap = entryById;
final contentId = entry.contentId; saved.forEach((metadata) => idMap[metadata.contentId]?.catalogMetadata = metadata);
entry.catalogMetadata = saved.firstWhereOrNull((metadata) => metadata.contentId == contentId);
});
debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries'); debugPrint('$runtimeType loadCatalogMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${saved.length} entries');
onCatalogMetadataChanged(); onCatalogMetadataChanged();
} }

View file

@ -95,6 +95,14 @@ class _AppDebugPageState extends State<AppDebugPage> {
}, },
title: const Text('Show tasks overlay'), 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(), const Divider(),
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),

View file

@ -17,7 +17,7 @@ class DebugAppDatabaseSection extends StatefulWidget {
class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with AutomaticKeepAliveClientMixin { class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with AutomaticKeepAliveClientMixin {
late Future<int> _dbFileSizeLoader; late Future<int> _dbFileSizeLoader;
late Future<Set<AvesEntry>> _dbEntryLoader; late Future<Set<AvesEntry>> _dbEntryLoader;
late Future<List<DateMetadata>> _dbDateLoader; late Future<Map<int?, int?>> _dbDateLoader;
late Future<List<CatalogMetadata>> _dbMetadataLoader; late Future<List<CatalogMetadata>> _dbMetadataLoader;
late Future<List<AddressDetails>> _dbAddressLoader; late Future<List<AddressDetails>> _dbAddressLoader;
late Future<Set<FavouriteRow>> _dbFavouritesLoader; late Future<Set<FavouriteRow>> _dbFavouritesLoader;
@ -82,7 +82,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
); );
}, },
), ),
FutureBuilder<List>( FutureBuilder<Map<int?, int?>>(
future: _dbDateLoader, future: _dbDateLoader,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.hasError) return Text(snapshot.error.toString());

View file

@ -18,7 +18,7 @@ class DbTab extends StatefulWidget {
} }
class _DbTabState extends State<DbTab> { class _DbTabState extends State<DbTab> {
late Future<DateMetadata?> _dbDateLoader; late Future<int?> _dbDateLoader;
late Future<AvesEntry?> _dbEntryLoader; late Future<AvesEntry?> _dbEntryLoader;
late Future<CatalogMetadata?> _dbMetadataLoader; late Future<CatalogMetadata?> _dbMetadataLoader;
late Future<AddressDetails?> _dbAddressLoader; late Future<AddressDetails?> _dbAddressLoader;
@ -33,7 +33,7 @@ class _DbTabState extends State<DbTab> {
void _loadDatabase() { void _loadDatabase() {
final contentId = entry.contentId; 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)); _dbEntryLoader = metadataDb.loadEntries().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
_dbMetadataLoader = metadataDb.loadMetadataEntries().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)); _dbAddressLoader = metadataDb.loadAddresses().then((values) => values.firstWhereOrNull((row) => row.contentId == contentId));
@ -45,7 +45,7 @@ class _DbTabState extends State<DbTab> {
return ListView( return ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
FutureBuilder<DateMetadata?>( FutureBuilder<int?>(
future: _dbDateLoader, future: _dbDateLoader,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString()); if (snapshot.hasError) return Text(snapshot.error.toString());
@ -58,7 +58,7 @@ class _DbTabState extends State<DbTab> {
if (data != null) if (data != null)
InfoRowGroup( InfoRowGroup(
info: { info: {
'dateMillis': '${data.dateMillis}', 'dateMillis': '$data',
}, },
), ),
], ],

View file

@ -24,7 +24,7 @@ class FakeMetadataDb extends Fake implements MetadataDb {
Future<void> updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(null); Future<void> updateEntryId(int oldId, AvesEntry entry) => SynchronousFuture(null);
@override @override
Future<List<DateMetadata>> loadDates() => SynchronousFuture([]); Future<Map<int?, int?>> loadDates() => SynchronousFuture({});
@override @override
Future<List<CatalogMetadata>> loadMetadataEntries() => SynchronousFuture([]); Future<List<CatalogMetadata>> loadMetadataEntries() => SynchronousFuture([]);