diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a6c8648f..8d9ec7c6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -11,28 +11,28 @@ environment: dependencies: auto_size_text: ^3.0.0 - badges: ^3.0.2 + badges: ^3.1.2 better_open_file: ^3.6.4 - collection: ^1.17.1 + collection: ^1.18.0 dart_earcut: ^1.0.1 file_picker: ^5.2.10 flutter: sdk: flutter - flutter_foreground_task: ^6.0.0+1 - flutter_map: ^6.0.0 + flutter_foreground_task: ^6.1.2 + flutter_map: ^6.1.0 flutter_map_animations: ^0.5.3 - flutter_map_tile_caching: + flutter_map_tile_caching: ^9.0.0-dev.5 flutter_speed_dial: ^7.0.0 fmtc_plus_sharing: ^8.0.0 - google_fonts: ^5.1.0 + google_fonts: ^6.1.0 gpx: ^2.2.1 - http: ^1.0.0 - intl: ^0.18.0 + http: ^1.1.2 + intl: ^0.19.0 latlong2: ^0.9.0 osm_nominatim: ^3.0.0 - path: ^1.8.3 - provider: ^6.0.3 - stream_transform: ^2.0.0 + path: ^1.9.0 + provider: ^6.1.1 + stream_transform: ^2.1.0 validators: ^3.0.0 version: ^3.0.2 diff --git a/lib/src/backend/impl_tools/no_sync.dart b/lib/src/backend/impl_tools/no_sync.dart index b537cf60..5c101af9 100644 --- a/lib/src/backend/impl_tools/no_sync.dart +++ b/lib/src/backend/impl_tools/no_sync.dart @@ -98,6 +98,28 @@ mixin FMTCBackendNoSync implements FMTCBackendInternal { @override final supportsSyncGetStoreLength = false; + /// This synchronous method is unsupported by this implementation - use + /// [getStoreHits] instead + @override + int getStoreHitsSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncGetStoreHits = false; + + /// This synchronous method is unsupported by this implementation - use + /// [getStoreMisses] instead + @override + int getStoreMissesSync({ + required String storeName, + }) => + throw SyncOperationUnsupported(); + + @override + final supportsSyncGetStoreMisses = false; + /// This synchronous method is unsupported by this implementation - use /// [readTile] instead @override diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index 095d6e24..5333d56d 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; -import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart' as meta; import 'package:path_provider/path_provider.dart'; @@ -46,13 +45,10 @@ class _ObjectBoxBackendImpl // for (final v in _WorkerKey.values) v: null, //}; - // TODO: Make cmds that need to be idempotent for performance, like - // delete oldest tile - - // `deleteOldestTile` - int _numberOfOldestTilesToDelete = 0; - Timer? _deleteOldestTileTimer; - String? _storeNameOfCurrentDeleteOldestTimer; + // `deleteOldestTile` tracking & debouncing + int _dotLength = 0; + Timer? _dotDebouncer; + String? _dotStore; Future<_WorkerRes> sendCmd(_WorkerCmd cmd) async { expectInitialised; @@ -187,6 +183,24 @@ class _ObjectBoxBackendImpl )) .data!['length']! as int; + @override + Future getStoreHits({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.getStoreHits, args: {'storeName': storeName}), + )) + .data!['hits']! as int; + + @override + Future getStoreMisses({ + required String storeName, + }) async => + (await sendCmd( + (key: _WorkerKey.getStoreMisses, args: {'storeName': storeName}), + )) + .data!['misses']! as int; + @override Future readTile({ required String url, @@ -226,11 +240,11 @@ class _ObjectBoxBackendImpl key: _WorkerKey.removeOldestTile, args: { 'storeName': storeName, - 'number': _numberOfOldestTilesToDelete, + 'number': _dotLength, } ), ); - _numberOfOldestTilesToDelete = 0; + _dotLength = 0; } @override @@ -240,26 +254,26 @@ class _ObjectBoxBackendImpl // Attempts to avoid flooding worker with requests to delete oldest tile, // and 'batches' them instead - if (_storeNameOfCurrentDeleteOldestTimer != storeName) { + if (_dotStore != storeName) { // If the store has changed, failing to reset the batch/queue will mean // tiles are removed from the wrong store - _storeNameOfCurrentDeleteOldestTimer = storeName; - if (_deleteOldestTileTimer != null && _deleteOldestTileTimer!.isActive) { - _deleteOldestTileTimer!.cancel(); + _dotStore = storeName; + if (_dotDebouncer != null && _dotDebouncer!.isActive) { + _dotDebouncer!.cancel(); _sendRemoveOldestTileCmd(storeName); } } - if (_deleteOldestTileTimer != null && _deleteOldestTileTimer!.isActive) { - _deleteOldestTileTimer!.cancel(); - _deleteOldestTileTimer = Timer( + if (_dotDebouncer != null && _dotDebouncer!.isActive) { + _dotDebouncer!.cancel(); + _dotDebouncer = Timer( const Duration(milliseconds: 500), () => _sendRemoveOldestTileCmd(storeName), ); return; } - _deleteOldestTileTimer = Timer( + _dotDebouncer = Timer( const Duration(seconds: 1), () => _sendRemoveOldestTileCmd(storeName), ); diff --git a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json index b75bfe65..edb674e3 100644 --- a/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json +++ b/lib/src/backend/impls/objectbox/models/generated/objectbox-model.json @@ -5,7 +5,7 @@ "entities": [ { "id": "1:7419244569066266196", - "lastPropertyId": "4:3677248801338209880", + "lastPropertyId": "6:3446172587861422549", "name": "ObjectBoxStore", "properties": [ { @@ -30,6 +30,16 @@ "id": "4:3677248801338209880", "name": "numberOfBytes", "type": 8 + }, + { + "id": "5:1702586868505261124", + "name": "hits", + "type": 6 + }, + { + "id": "6:3446172587861422549", + "name": "misses", + "type": 6 } ], "relations": [] diff --git a/lib/src/backend/impls/objectbox/models/models.dart b/lib/src/backend/impls/objectbox/models/models.dart index eeb080f1..5db648e1 100644 --- a/lib/src/backend/impls/objectbox/models/models.dart +++ b/lib/src/backend/impls/objectbox/models/models.dart @@ -14,10 +14,18 @@ base class ObjectBoxStore extends BackendStore { @Unique() String name; + @override int numberOfTiles; + @override double numberOfBytes; + @override + int hits; + + @override + int misses; + @Index() @Backlink() final tiles = ToMany(); @@ -26,6 +34,8 @@ base class ObjectBoxStore extends BackendStore { required this.name, required this.numberOfTiles, required this.numberOfBytes, + required this.hits, + required this.misses, }); } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index 5abcb03c..fc3a4578 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -12,6 +12,8 @@ enum _WorkerKey { deleteStore, getStoreSize, getStoreLength, + getStoreHits, + getStoreMisses, readTile, writeTile, deleteTile, @@ -68,6 +70,8 @@ Future _worker( name: cmd.args['storeName']! as String, numberOfTiles: 0, numberOfBytes: 0, + hits: 0, + misses: 0, ), mode: PutMode.insert, ); @@ -78,6 +82,7 @@ Future _worker( final removeIds = []; final tiles = root.box(); + final stores = root.box(); final tilesQuery = (tiles.query() ..linkMany( @@ -86,6 +91,9 @@ Future _worker( )) .build(); + final storeQuery = + stores.query(ObjectBoxStore_.name.equals(storeName)).build(); + root.runInTransaction( TxMode.write, () { @@ -107,6 +115,21 @@ Future _worker( tiles.query(ObjectBoxTile_.id.oneOf(removeIds)).build() ..remove() ..close(); + + final store = storeQuery.findUnique() ?? + (throw StoreUnavailable(storeName: storeName)); + storeQuery.close(); + + assert(store.tiles.isEmpty); + + stores.put( + store + ..tiles.clear() + ..numberOfTiles = 0 + ..numberOfBytes = 0 + ..hits = 0 + ..misses = 0, + ); }, ); sendRes((key: cmd.key, data: null)); @@ -168,6 +191,34 @@ Future _worker( sendRes((key: cmd.key, data: {'length': length})); break; + case _WorkerKey.getStoreHits: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final hits = (query.findUnique() ?? + (throw StoreUnavailable(storeName: storeName))) + .hits; + query.close(); + + sendRes((key: cmd.key, data: {'hits': hits})); + break; + case _WorkerKey.getStoreMisses: + final storeName = cmd.args['storeName']! as String; + + final query = root + .box() + .query(ObjectBoxStore_.name.equals(storeName)) + .build(); + final misses = (query.findUnique() ?? + (throw StoreUnavailable(storeName: storeName))) + .misses; + query.close(); + + sendRes((key: cmd.key, data: {'misses': misses})); + break; case _WorkerKey.readTile: final query = root .box() @@ -176,6 +227,8 @@ Future _worker( final tile = query.findUnique(); query.close(); + // TODO: Hits & misses + sendRes((key: cmd.key, data: {'tile': tile})); break; case _WorkerKey.writeTile: diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index 61d7202f..cd802ea6 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -26,12 +26,12 @@ import 'models.dart'; /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend /// * Not all sync versions of methods are guaranteed to have implementations -/// * Never access the [internal] method of a backend -abstract interface class FMTCBackend { +/// * Avoid calling the [internal] method of a backend +abstract interface class FMTCBackend { const FMTCBackend(); @protected - FMTCBackendInternal get internal; + Internal get internal; } /// An abstract interface that FMTC will use to communicate with a storage @@ -202,6 +202,40 @@ abstract interface class FMTCBackendInternal { /// If `false`, calling will throw an [SyncOperationUnsupported] error. abstract final bool supportsSyncGetStoreLength; + /// Retrieve the number of times that a tile was successfully retrieved from + /// the specified store when browsing + Future getStoreHits({ + required String storeName, + }); + + /// Retrieve the number of times that a tile was successfully retrieved from + /// the specified store when browsing + int getStoreHitsSync({ + required String storeName, + }); + + /// Whether [getStoreHitsSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncGetStoreHits; + + /// Retrieve the number of times that a tile was attempted to be retrieved from + /// the specified store when browsing, but was not present + Future getStoreMisses({ + required String storeName, + }); + + /// Retrieve the number of times that a tile was attempted to be retrieved from + /// the specified store when browsing, but was not present + int getStoreMissesSync({ + required String storeName, + }); + + /// Whether [getStoreMissesSync] is implemented + /// + /// If `false`, calling will throw an [SyncOperationUnsupported] error. + abstract final bool supportsSyncGetStoreMisses; + /// Get a raw tile by URL Future readTile({ required String url, diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index ff7dfa6d..bbc67d33 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -1,15 +1,26 @@ import 'dart:typed_data'; +import 'package:meta/meta.dart'; + abstract base class BackendStore { abstract String name; + abstract int numberOfTiles; + abstract double numberOfBytes; + abstract int hits; + abstract int misses; /// Uses [name] for equality comparisons only (unless the two objects are /// [identical]) + /// + /// Overriding this in an implementation may cause FMTC logic to break, and is + /// therefore not recommended. @override + @nonVirtual bool operator ==(Object? other) => identical(this, other) || (other is BackendStore && name == other.name); @override + @nonVirtual int get hashCode => name.hashCode; } @@ -20,10 +31,15 @@ abstract base class BackendTile { /// Uses [url] for equality comparisons only (unless the two objects are /// [identical]) + /// + /// Overriding this in an implementation may cause FMTC logic to break, and is + /// therefore not recommended. @override + @nonVirtual bool operator ==(Object? other) => identical(this, other) || (other is BackendTile && url == other.url); @override + @nonVirtual int get hashCode => url.hashCode; } diff --git a/lib/src/misc/with_backend_access.dart b/lib/src/misc/with_backend_access.dart index f6483a36..e95d512e 100644 --- a/lib/src/misc/with_backend_access.dart +++ b/lib/src/misc/with_backend_access.dart @@ -1,14 +1,13 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -// ignore_for_file: invalid_use_of_protected_member - part of flutter_map_tile_caching; abstract base class _WithBackendAccess { const _WithBackendAccess(this._store); final StoreDirectory _store; + // ignore: invalid_use_of_protected_member FMTCBackendInternal get _backend => FMTC.instance.settings.backend.internal; String get _storeName => _store.storeName; } diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index b83f6259..83adfc16 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -5,16 +5,10 @@ part of flutter_map_tile_caching; /// Manages a [StoreDirectory]'s representation on the filesystem, such as /// creation and deletion -class StoreManagement { - StoreManagement._(StoreDirectory storeDirectory) - : _name = storeDirectory.storeName, - _id = DatabaseTools.hash(storeDirectory.storeName), - _registry = FMTCRegistry.instance, - _rootDirectory = FMTC.instance.rootDirectory.directory; +final class StoreManagement extends _WithBackendAccess { + StoreManagement._(super.store) + : _rootDirectory = FMTC.instance.rootDirectory.directory; - final String _name; - final int _id; - final FMTCRegistry _registry; final Directory _rootDirectory; /// Check whether this store is ready for use diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a6172b42..a8e9eaaa 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -14,39 +14,42 @@ final class StoreStats extends _WithBackendAccess { double get storeSize => _backend.getStoreSizeSync(storeName: _storeName); /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) - Future get storeSizeAsync async => + Future get storeSizeAsync => _backend.getStoreSize(storeName: _storeName); /// Retrieve the number of stored tiles synchronously /// /// Prefer [storeLengthAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - int get storeLength => _db.tiles.countSync(); + int get storeLength => _backend.getStoreLengthSync(storeName: _storeName); /// Retrieve the number of stored tiles asynchronously - Future get storeLengthAsync => _db.tiles.count(); + Future get storeLengthAsync => + _backend.getStoreLength(storeName: _storeName); /// Retrieve the number of tiles that were successfully retrieved from the /// store during browsing synchronously /// /// Prefer [cacheHitsAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - int get cacheHits => _db.descriptorSync.hits; + int get cacheHits => _backend.getStoreHitsSync(storeName: _storeName); /// Retrieve the number of tiles that were successfully retrieved from the /// store during browsing asynchronously - Future get cacheHitsAsync async => (await _db.descriptor).hits; + Future get cacheHitsAsync async => + _backend.getStoreHits(storeName: _storeName); /// Retrieve the number of tiles that were unsuccessfully retrieved from the /// store during browsing synchronously /// /// Prefer [cacheMissesAsync] to avoid blocking the UI thread. Otherwise, this /// has slightly better performance. - int get cacheMisses => _db.descriptorSync.misses; + int get cacheMisses => _backend.getStoreMissesSync(storeName: _storeName); /// Retrieve the number of tiles that were unsuccessfully retrieved from the /// store during browsing asynchronously - Future get cacheMissesAsync async => (await _db.descriptor).misses; + Future get cacheMissesAsync async => + _backend.getStoreMisses(storeName: _storeName); /// Watch for changes in the current store /// @@ -74,15 +77,7 @@ final class StoreStats extends _WithBackendAccess { StoreParts.stats, ], }) => - StreamGroup.merge([ - if (storeParts.contains(StoreParts.metadata)) - _db.metadata.watchLazy(fireImmediately: fireImmediately), - if (storeParts.contains(StoreParts.tiles)) - _db.tiles.watchLazy(fireImmediately: fireImmediately), - if (storeParts.contains(StoreParts.stats)) - _db.storeDescriptor - .watchObjectLazy(0, fireImmediately: fireImmediately), - ]).debounce(debounce ?? Duration.zero); + throw UnimplementedError(); } /// Parts of a store which can be watched diff --git a/pubspec.yaml b/pubspec.yaml index 79768e34..7219eead 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,24 +32,24 @@ dependencies: dart_earcut: ^1.0.1 flutter: sdk: flutter - flutter_map: ^6.0.1 - http: ^1.1.0 + flutter_map: ^6.1.0 + http: ^1.1.2 isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 latlong2: ^0.9.0 meta: ^1.11.0 - objectbox: ^2.3.1 + objectbox: ^2.4.0 objectbox_flutter_libs: any - path: ^1.8.3 + path: ^1.9.0 path_provider: ^2.1.1 queue: ^3.1.0+2 stream_transform: ^2.1.0 watcher: ^1.1.0 dev_dependencies: - build_runner: ^2.4.6 - objectbox_generator: ^2.3.1 - test: ^1.24.9 + build_runner: ^2.4.7 + objectbox_generator: ^2.4.0 + test: ^1.25.0 flutter: null