diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index b228b8707a1..edbdbad2c49 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -1526,11 +1526,11 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/gRPCCertificates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/gRPCCertificates-Firestore.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Firestore.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1785,11 +1785,11 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/gRPCCertificates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/gRPCCertificates-Firestore.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/gRPCCertificates-Firestore.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h index 805bd0cc20a..171ac33d436 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h @@ -16,13 +16,15 @@ #import +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" + @protocol FSTPersistence; NS_ASSUME_NONNULL_BEGIN @interface FSTLRUGarbageCollectorTests : XCTestCase -- (id)newPersistence; +- (id)newPersistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams; @property(nonatomic, strong) id persistence; diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm index e84dd974c0f..93f1968323d 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm @@ -40,6 +40,8 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::auth::User; +using firebase::firestore::local::LruParams; +using firebase::firestore::local::LruResults; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeyHash; using firebase::firestore::model::DocumentKeySet; @@ -79,9 +81,9 @@ - (BOOL)isTestBaseClass { return ([self class] == [FSTLRUGarbageCollectorTests class]); } -- (void)newTestResources { +- (void)newTestResourcesWithLruParams:(LruParams)lruParams { HARD_ASSERT(_persistence == nil, "Persistence already created"); - _persistence = [self newPersistence]; + _persistence = [self newPersistenceWithLruParams:lruParams]; _queryCache = [_persistence queryCache]; _documentCache = [_persistence remoteDocumentCache]; _mutationQueue = [_persistence mutationQueueForUser:_user]; @@ -93,7 +95,11 @@ - (void)newTestResources { }); } -- (id)newPersistence { +- (void)newTestResources { + [self newTestResourcesWithLruParams:LruParams::Default()]; +} + +- (id)newPersistenceWithLruParams:(LruParams)lruParams { @throw FSTAbstractMethodException(); // NOLINT } @@ -630,6 +636,7 @@ - (void)testRemoveTargetsThenGC { // Finally, do the garbage collection, up to but not including the removal of middleTarget NSDictionary *liveQueries = @{@(oldestTarget.targetID) : oldestTarget}; + int queriesRemoved = [self removeQueriesThroughSequenceNumber:upperBound liveQueries:liveQueries]; XCTAssertEqual(1, queriesRemoved, @"Expected to remove newest target"); int docsRemoved = [self removeOrphanedDocumentsThroughSequenceNumber:upperBound]; @@ -672,6 +679,86 @@ - (void)testGetsSize { [_persistence shutdown]; } +- (void)testDisabled { + if ([self isTestBaseClass]) return; + + LruParams params = LruParams::Disabled(); + [self newTestResourcesWithLruParams:params]; + + _persistence.run("fill cache", [&]() { + // Simulate a bunch of ack'd mutations + for (int i = 0; i < 500; i++) { + FSTDocument *doc = [self cacheADocumentInTransaction]; + [self markDocumentEligibleForGCInTransaction:doc.key]; + } + }); + + LruResults results = + _persistence.run("GC", [&]() -> LruResults { return [_gc collectWithLiveTargets:@{}]; }); + XCTAssertFalse(results.didRun); + + [_persistence shutdown]; +} + +- (void)testCacheTooSmall { + if ([self isTestBaseClass]) return; + + LruParams params = LruParams::Default(); + [self newTestResourcesWithLruParams:params]; + + _persistence.run("fill cache", [&]() { + // Simulate a bunch of ack'd mutations + for (int i = 0; i < 50; i++) { + FSTDocument *doc = [self cacheADocumentInTransaction]; + [self markDocumentEligibleForGCInTransaction:doc.key]; + } + }); + + int cacheSize = (int)[_gc byteSize]; + // Verify that we don't have enough in our cache to warrant collection + XCTAssertLessThan(cacheSize, params.minBytesThreshold); + + // Try collection and verify that it didn't run + LruResults results = + _persistence.run("GC", [&]() -> LruResults { return [_gc collectWithLiveTargets:@{}]; }); + XCTAssertFalse(results.didRun); + + [_persistence shutdown]; +} + +- (void)testGCRan { + if ([self isTestBaseClass]) return; + + LruParams params = LruParams::Default(); + // Set a low threshold so we will definitely run + params.minBytesThreshold = 100; + [self newTestResourcesWithLruParams:params]; + + // Add 100 targets and 10 documents to each + for (int i = 0; i < 100; i++) { + // Use separate transactions so that each target and associated documents get their own + // sequence number. + _persistence.run("Add a target and some documents", [&]() { + FSTQueryData *queryData = [self addNextQueryInTransaction]; + for (int j = 0; j < 10; j++) { + FSTDocument *doc = [self cacheADocumentInTransaction]; + [self addDocument:doc.key toTarget:queryData.targetID]; + } + }); + } + + // Mark nothing as live, so everything is eligible. + LruResults results = + _persistence.run("GC", [&]() -> LruResults { return [_gc collectWithLiveTargets:@{}]; }); + + // By default, we collect 10% of the sequence numbers. Since we added 100 targets, + // that should be 10 targets with 10 documents each, for a total of 100 documents. + XCTAssertTrue(results.didRun); + XCTAssertEqual(10, results.targetsRemoved); + XCTAssertEqual(100, results.documentsRemoved); + [_persistence shutdown]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm index ccc3e16dc2c..d680e1fecf6 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm @@ -19,6 +19,7 @@ #import "Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLevelDB.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" @@ -26,6 +27,8 @@ using firebase::firestore::local::LevelDbDocumentTargetKey; using firebase::firestore::model::DocumentKey; +using firebase::firestore::local::LruParams; + NS_ASSUME_NONNULL_BEGIN @interface FSTLevelDBLRUGarbageCollectorTests : FSTLRUGarbageCollectorTests @@ -33,8 +36,8 @@ @interface FSTLevelDBLRUGarbageCollectorTests : FSTLRUGarbageCollectorTests @implementation FSTLevelDBLRUGarbageCollectorTests -- (id)newPersistence { - return [FSTPersistenceTestHelpers levelDBPersistence]; +- (id)newPersistenceWithLruParams:(LruParams)lruParams { + return [FSTPersistenceTestHelpers levelDBPersistenceWithLruParams:lruParams]; } - (BOOL)sentinelExists:(const DocumentKey &)key { diff --git a/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm index 66226d4da9d..360e4225dff 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm @@ -17,11 +17,14 @@ #import "Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTMemoryPersistence.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::local::LruParams; + NS_ASSUME_NONNULL_BEGIN @interface FSTMemoryLRUGarbageCollectionTests : FSTLRUGarbageCollectorTests @@ -29,8 +32,8 @@ @interface FSTMemoryLRUGarbageCollectionTests : FSTLRUGarbageCollectorTests @implementation FSTMemoryLRUGarbageCollectionTests -- (id)newPersistence { - return [FSTPersistenceTestHelpers lruMemoryPersistence]; +- (id)newPersistenceWithLruParams:(LruParams)lruParams { + return [FSTPersistenceTestHelpers lruMemoryPersistenceWithLruParams:lruParams]; } - (BOOL)sentinelExists:(const DocumentKey &)key { diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h index 6b7a6060705..8d5937afc67 100644 --- a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h @@ -16,6 +16,7 @@ #import +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #include "Firestore/core/src/firebase/firestore/util/path.h" @class FSTLevelDB; @@ -48,10 +49,21 @@ NS_ASSUME_NONNULL_BEGIN */ + (FSTLevelDB *)levelDBPersistenceWithDir:(firebase::firestore::util::Path)dir; +/** + * Creates and starts a new FSTLevelDB instance for testing, destroying any previous contents + * if they existed. + * + * Sets up the LRU garbage collection to use the provided params. + */ ++ (FSTLevelDB *)levelDBPersistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams; + /** Creates and starts a new FSTMemoryPersistence instance for testing. */ + (FSTMemoryPersistence *)eagerGCMemoryPersistence; + (FSTMemoryPersistence *)lruMemoryPersistence; + ++ (FSTMemoryPersistence *)lruMemoryPersistenceWithLruParams: + (firebase::firestore::local::LruParams)lruParams; @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm index c544ff6f05a..74059c33d61 100644 --- a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm @@ -18,6 +18,7 @@ #include +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" #import "Firestore/Source/Local/FSTMemoryPersistence.h" @@ -30,6 +31,7 @@ #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; +using firebase::firestore::local::LruParams; using firebase::firestore::model::DatabaseId; using firebase::firestore::util::Path; using firebase::firestore::util::Status; @@ -61,8 +63,13 @@ + (Path)levelDBDir { } + (FSTLevelDB *)levelDBPersistenceWithDir:(Path)dir { + return [self levelDBPersistenceWithDir:dir lruParams:LruParams::Default()]; +} + ++ (FSTLevelDB *)levelDBPersistenceWithDir:(Path)dir lruParams:(LruParams)params { FSTLocalSerializer *serializer = [self localSerializer]; - FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:std::move(dir) serializer:serializer]; + FSTLevelDB *db = + [[FSTLevelDB alloc] initWithDirectory:std::move(dir) serializer:serializer lruParams:params]; Status status = [db start]; if (!status.ok()) { [NSException raise:NSInternalInconsistencyException @@ -72,6 +79,10 @@ + (FSTLevelDB *)levelDBPersistenceWithDir:(Path)dir { return db; } ++ (FSTLevelDB *)levelDBPersistenceWithLruParams:(LruParams)lruParams { + return [self levelDBPersistenceWithDir:[self levelDBDir] lruParams:lruParams]; +} + + (FSTLevelDB *)levelDBPersistence { return [self levelDBPersistenceWithDir:[self levelDBDir]]; } @@ -88,9 +99,13 @@ + (FSTMemoryPersistence *)eagerGCMemoryPersistence { } + (FSTMemoryPersistence *)lruMemoryPersistence { + return [self lruMemoryPersistenceWithLruParams:LruParams::Default()]; +} + ++ (FSTMemoryPersistence *)lruMemoryPersistenceWithLruParams:(LruParams)lruParams { FSTLocalSerializer *serializer = [self localSerializer]; FSTMemoryPersistence *persistence = - [FSTMemoryPersistence persistenceWithLRUGCAndSerializer:serializer]; + [FSTMemoryPersistence persistenceWithLruParams:lruParams serializer:serializer]; Status status = [persistence start]; if (!status.ok()) { [NSException raise:NSInternalInconsistencyException diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm index 6fac8a4536a..a98c03cb2a9 100644 --- a/Firestore/Source/API/FIRFirestore.mm +++ b/Firestore/Source/API/FIRFirestore.mm @@ -265,7 +265,7 @@ const DatabaseInfo database_info(*self.databaseID, util::MakeString(_persistence HARD_ASSERT(_workerQueue, "Expected non-null _workerQueue"); _client = [FSTFirestoreClient clientWithDatabaseInfo:database_info - usePersistence:_settings.persistenceEnabled + settings:_settings credentialsProvider:_credentialsProvider.get() userExecutor:std::move(userExecutor) workerQueue:std::move(_workerQueue)]; diff --git a/Firestore/Source/API/FIRFirestoreSettings.mm b/Firestore/Source/API/FIRFirestoreSettings.mm index 8f998ec2684..a0efb0b7490 100644 --- a/Firestore/Source/API/FIRFirestoreSettings.mm +++ b/Firestore/Source/API/FIRFirestoreSettings.mm @@ -23,6 +23,8 @@ static NSString *const kDefaultHost = @"firestore.googleapis.com"; static const BOOL kDefaultSSLEnabled = YES; static const BOOL kDefaultPersistenceEnabled = YES; +static const int64_t kDefaultCacheSizeBytes = 100 * 1024 * 1024; +static const int64_t kMinimumCacheSizeBytes = 1 * 1024 * 1024; // TODO(b/73820332): flip the default. static const BOOL kDefaultTimestampsInSnapshotsEnabled = NO; @@ -35,6 +37,7 @@ - (instancetype)init { _dispatchQueue = dispatch_get_main_queue(); _persistenceEnabled = kDefaultPersistenceEnabled; _timestampsInSnapshotsEnabled = kDefaultTimestampsInSnapshotsEnabled; + _cacheSizeBytes = kDefaultCacheSizeBytes; } return self; } @@ -51,7 +54,8 @@ - (BOOL)isEqual:(id)other { self.isSSLEnabled == otherSettings.isSSLEnabled && self.dispatchQueue == otherSettings.dispatchQueue && self.isPersistenceEnabled == otherSettings.isPersistenceEnabled && - self.timestampsInSnapshotsEnabled == otherSettings.timestampsInSnapshotsEnabled; + self.timestampsInSnapshotsEnabled == otherSettings.timestampsInSnapshotsEnabled && + self.cacheSizeBytes == otherSettings.cacheSizeBytes; } - (NSUInteger)hash { @@ -60,6 +64,7 @@ - (NSUInteger)hash { // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); result = 31 * result + (self.timestampsInSnapshotsEnabled ? 1231 : 1237); + result = 31 * result + (NSUInteger)self.cacheSizeBytes; return result; } @@ -70,6 +75,7 @@ - (id)copyWithZone:(nullable NSZone *)zone { copy.dispatchQueue = _dispatchQueue; copy.persistenceEnabled = _persistenceEnabled; copy.timestampsInSnapshotsEnabled = _timestampsInSnapshotsEnabled; + copy.cacheSizeBytes = _cacheSizeBytes; return copy; } @@ -93,6 +99,14 @@ - (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { _dispatchQueue = dispatchQueue; } +- (void)setCacheSizeBytes:(int64_t)cacheSizeBytes { + if (cacheSizeBytes != kFIRFirestoreCacheSizeUnlimited && + cacheSizeBytes < kMinimumCacheSizeBytes) { + FSTThrowInvalidArgument(@"Cache size must be set to at least %i bytes", kMinimumCacheSizeBytes); + } + _cacheSizeBytes = cacheSizeBytes; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index f8314f32afa..f4d850dda6d 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -30,6 +30,7 @@ @class FIRDocumentReference; @class FIRDocumentSnapshot; +@class FIRFirestoreSettings; @class FIRQuery; @class FIRQuerySnapshot; @class FSTDatabaseID; @@ -57,7 +58,7 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype) clientWithDatabaseInfo:(const firebase::firestore::core::DatabaseInfo &)databaseInfo - usePersistence:(BOOL)usePersistence + settings:(FIRFirestoreSettings *)settings credentialsProvider:(firebase::firestore::auth::CredentialsProvider *) credentialsProvider // no passing ownership userExecutor:(std::unique_ptr)userExecutor diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index e82cae926b7..c72eae6cc18 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -16,11 +16,13 @@ #import "Firestore/Source/Core/FSTFirestoreClient.h" +#include // NOLINT(build/c++11) #include // NOLINT(build/c++11) #include #include #import "FIRFirestoreErrors.h" +#import "FIRFirestoreSettings.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" #import "Firestore/Source/API/FIRQuery+Internal.h" @@ -31,6 +33,7 @@ #import "Firestore/Source/Core/FSTSyncEngine.h" #import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Core/FSTView.h" +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" #import "Firestore/Source/Local/FSTLocalStore.h" @@ -54,22 +57,30 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::local::LruParams; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::OnlineState; using firebase::firestore::util::Path; using firebase::firestore::util::Status; using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::DelayedOperation; using firebase::firestore::util::Executor; +using firebase::firestore::util::TimerId; NS_ASSUME_NONNULL_BEGIN +/** How long we wait to try running LRU GC after SDK initialization. */ +static const std::chrono::milliseconds FSTLruGcInitialDelay = std::chrono::minutes(1); +/** Minimum amount of time between GC checks, after the first one. */ +static const std::chrono::milliseconds FSTLruGcRegularDelay = std::chrono::minutes(5); + @interface FSTFirestoreClient () { DatabaseInfo _databaseInfo; } - (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo - usePersistence:(BOOL)usePersistence + settings:(FIRFirestoreSettings *)settings credentialsProvider: (CredentialsProvider *)credentialsProvider // no passing ownership userExecutor:(std::unique_ptr)userExecutor @@ -97,6 +108,11 @@ @implementation FSTFirestoreClient { std::unique_ptr _workerQueue; std::unique_ptr _userExecutor; + std::chrono::milliseconds _initialGcDelay; + std::chrono::milliseconds _regularGcDelay; + BOOL _gcHasRun; + _Nullable id _lruDelegate; + DelayedOperation _lruCallback; } - (Executor *)userExecutor { @@ -108,20 +124,20 @@ - (AsyncQueue *)workerQueue { } + (instancetype)clientWithDatabaseInfo:(const DatabaseInfo &)databaseInfo - usePersistence:(BOOL)usePersistence + settings:(FIRFirestoreSettings *)settings credentialsProvider: (CredentialsProvider *)credentialsProvider // no passing ownership userExecutor:(std::unique_ptr)userExecutor workerQueue:(std::unique_ptr)workerQueue { return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo - usePersistence:usePersistence + settings:settings credentialsProvider:credentialsProvider userExecutor:std::move(userExecutor) workerQueue:std::move(workerQueue)]; } - (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo - usePersistence:(BOOL)usePersistence + settings:(FIRFirestoreSettings *)settings credentialsProvider: (CredentialsProvider *)credentialsProvider // no passing ownership userExecutor:(std::unique_ptr)userExecutor @@ -131,6 +147,9 @@ - (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo _credentialsProvider = credentialsProvider; _userExecutor = std::move(userExecutor); _workerQueue = std::move(workerQueue); + _gcHasRun = NO; + _initialGcDelay = FSTLruGcInitialDelay; + _regularGcDelay = FSTLruGcRegularDelay; auto userPromise = std::make_shared>(); bool initialized = false; @@ -154,15 +173,15 @@ - (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo // Defer initialization until we get the current user from the credentialChangeListener. This is // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized // before any subsequently queued work runs. - _workerQueue->Enqueue([self, userPromise, usePersistence] { + _workerQueue->Enqueue([self, userPromise, settings] { User user = userPromise->get_future().get(); - [self initializeWithUser:user usePersistence:usePersistence]; + [self initializeWithUser:user settings:settings]; }); } return self; } -- (void)initializeWithUser:(const User &)user usePersistence:(BOOL)usePersistence { +- (void)initializeWithUser:(const User &)user settings:(FIRFirestoreSettings *)settings { // Do all of our initialization on our own dispatch queue. _workerQueue->VerifyIsCurrentQueue(); LOG_DEBUG("Initializing. Current user: %s", user.uid()); @@ -170,7 +189,7 @@ - (void)initializeWithUser:(const User &)user usePersistence:(BOOL)usePersistenc // Note: The initialization work must all be synchronous (we can't dispatch more work) since // external write/listen operations could get queued to run before that subsequent work // completes. - if (usePersistence) { + if (settings.isPersistenceEnabled) { Path dir = [FSTLevelDB storageDirectoryForDatabaseInfo:*self.databaseInfo documentsDirectory:[FSTLevelDB documentsDirectory]]; @@ -178,8 +197,13 @@ - (void)initializeWithUser:(const User &)user usePersistence:(BOOL)usePersistenc [[FSTSerializerBeta alloc] initWithDatabaseID:&self.databaseInfo->database_id()]; FSTLocalSerializer *serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; - - _persistence = [[FSTLevelDB alloc] initWithDirectory:std::move(dir) serializer:serializer]; + FSTLevelDB *ldb = + [[FSTLevelDB alloc] initWithDirectory:std::move(dir) + serializer:serializer + lruParams:LruParams::WithCacheSize(settings.cacheSizeBytes)]; + _lruDelegate = ldb.referenceDelegate; + _persistence = ldb; + [self scheduleLruGarbageCollection]; } else { _persistence = [FSTMemoryPersistence persistenceWithEagerGC]; } @@ -221,6 +245,19 @@ - (void)initializeWithUser:(const User &)user usePersistence:(BOOL)usePersistenc [_remoteStore start]; } +/** + * Schedules a callback to try running LRU garbage collection. Reschedules itself after the GC has + * run. + */ +- (void)scheduleLruGarbageCollection { + std::chrono::milliseconds delay = _gcHasRun ? _regularGcDelay : _initialGcDelay; + _lruCallback = _workerQueue->EnqueueAfterDelay(delay, TimerId::GarbageCollectionDelay, [self]() { + [self->_localStore collectGarbage:self->_lruDelegate.gc]; + self->_gcHasRun = YES; + [self scheduleLruGarbageCollection]; + }); +} + - (void)credentialDidChangeWithUser:(const User &)user { _workerQueue->VerifyIsCurrentQueue(); @@ -254,6 +291,10 @@ - (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { _workerQueue->Enqueue([self, completion] { self->_credentialsProvider->SetCredentialChangeListener(nullptr); + // If we've scheduled LRU garbage collection, cancel it. + if (self->_lruCallback) { + self->_lruCallback.Cancel(); + } [self.remoteStore shutdown]; [self.persistence shutdown]; if (completion) { diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.h b/Firestore/Source/Local/FSTLRUGarbageCollector.h index 39f41f3dd1e..1eeb5e6bd32 100644 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.h +++ b/Firestore/Source/Local/FSTLRUGarbageCollector.h @@ -16,6 +16,9 @@ #import +#include + +#import "FIRFirestoreSettings.h" #import "Firestore/Source/Local/FSTQueryData.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -26,6 +29,47 @@ extern const firebase::firestore::model::ListenSequenceNumber kFSTListenSequenceNumberInvalid; +namespace firebase { +namespace firestore { +namespace local { + +struct LruParams { + static const int64_t CacheSizeUnlimited = -1; + + static LruParams Default() { + return LruParams{100 * 1024 * 1024, 10, 1000}; + } + + static LruParams Disabled() { + return LruParams{kFIRFirestoreCacheSizeUnlimited, 0, 0}; + } + + static LruParams WithCacheSize(int64_t cacheSize) { + LruParams params = Default(); + params.minBytesThreshold = cacheSize; + return params; + } + + int64_t minBytesThreshold; + int percentileToCollect; + int maximumSequenceNumbersToCollect; +}; + +struct LruResults { + static LruResults DidNotRun() { + return LruResults{/* didRun= */ false, 0, 0, 0}; + } + + bool didRun; + int sequenceNumbersCollected; + int targetsRemoved; + int documentsRemoved; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + /** * Persistence layers intending to use LRU Garbage collection should implement this protocol. This * protocol defines the operations that the LRU garbage collector needs from the persistence layer. @@ -63,6 +107,9 @@ extern const firebase::firestore::model::ListenSequenceNumber kFSTListenSequence - (size_t)byteSize; +/** Returns the number of targets and orphaned documents cached. */ +- (int32_t)sequenceNumberCount; + /** Access to the underlying LRU Garbage collector instance. */ @property(strong, nonatomic, readonly) FSTLRUGarbageCollector *gc; @@ -74,8 +121,11 @@ extern const firebase::firestore::model::ListenSequenceNumber kFSTListenSequence */ @interface FSTLRUGarbageCollector : NSObject -- (instancetype)initWithQueryCache:(id)queryCache - delegate:(id)delegate; +- (instancetype)initWithDelegate:(id)delegate + params:(firebase::firestore::local::LruParams)params + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; /** * Given a target percentile, return the number of queries that make up that percentage of the @@ -107,4 +157,7 @@ extern const firebase::firestore::model::ListenSequenceNumber kFSTListenSequence - (size_t)byteSize; +- (firebase::firestore::local::LruResults)collectWithLiveTargets: + (NSDictionary *)liveTargets; + @end diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.mm b/Firestore/Source/Local/FSTLRUGarbageCollector.mm index 7f9b6b93d0c..31919a375d0 100644 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.mm +++ b/Firestore/Source/Local/FSTLRUGarbageCollector.mm @@ -16,17 +16,31 @@ #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" +#include //NOLINT(build/c++11) #include +#include #import "Firestore/Source/Local/FSTMutationQueue.h" +#import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Local/FSTQueryCache.h" +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +using Millis = std::chrono::milliseconds; +using firebase::Timestamp; +using firebase::firestore::local::LruParams; +using firebase::firestore::local::LruResults; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::ListenSequenceNumber; +const int64_t kFIRFirestoreCacheSizeUnlimited = LruParams::CacheSizeUnlimited; const ListenSequenceNumber kFSTListenSequenceNumberInvalid = -1; +static Millis::rep millisecondsBetween(const Timestamp &start, const Timestamp &end) { + return std::chrono::duration_cast(end.ToTimePoint() - start.ToTimePoint()).count(); +} + /** * RollingSequenceNumberBuffer tracks the nth sequence number in a series. Sequence numbers may be * added out of order. @@ -66,28 +80,75 @@ size_t size() const { const size_t max_elements_; }; -@interface FSTLRUGarbageCollector () - -@property(nonatomic, strong, readonly) id queryCache; - -@end - @implementation FSTLRUGarbageCollector { id _delegate; + LruParams _params; } -- (instancetype)initWithQueryCache:(id)queryCache - delegate:(id)delegate { +- (instancetype)initWithDelegate:(id)delegate params:(LruParams)params { self = [super init]; if (self) { - _queryCache = queryCache; _delegate = delegate; + _params = std::move(params); } return self; } +- (LruResults)collectWithLiveTargets:(NSDictionary *)liveTargets { + if (_params.minBytesThreshold == kFIRFirestoreCacheSizeUnlimited) { + LOG_DEBUG("Garbage collection skipped; disabled"); + return LruResults::DidNotRun(); + } + + size_t currentSize = [self byteSize]; + if (currentSize < _params.minBytesThreshold) { + // Not enough on disk to warrant collection. Wait another timeout cycle. + LOG_DEBUG("Garbage collection skipped; Cache size %s is lower than threshold %s", currentSize, + _params.minBytesThreshold); + return LruResults::DidNotRun(); + } else { + LOG_DEBUG("Running garbage collection on cache of size: %s", currentSize); + return [self runGCWithLiveTargets:liveTargets]; + } +} + +- (LruResults)runGCWithLiveTargets:(NSDictionary *)liveTargets { + Timestamp start = Timestamp::Now(); + int sequenceNumbers = [self queryCountForPercentile:_params.percentileToCollect]; + // Cap at the configured max + if (sequenceNumbers > _params.maximumSequenceNumbersToCollect) { + sequenceNumbers = _params.maximumSequenceNumbersToCollect; + } + Timestamp countedTargets = Timestamp::Now(); + + ListenSequenceNumber upperBound = [self sequenceNumberForQueryCount:sequenceNumbers]; + Timestamp foundUpperBound = Timestamp::Now(); + + int numTargetsRemoved = + [self removeQueriesUpThroughSequenceNumber:upperBound liveQueries:liveTargets]; + Timestamp removedTargets = Timestamp::Now(); + + int numDocumentsRemoved = [self removeOrphanedDocumentsThroughSequenceNumber:upperBound]; + Timestamp removedDocuments = Timestamp::Now(); + + std::string desc = "LRU Garbage Collection:\n"; + absl::StrAppend(&desc, "\tCounted targets in ", millisecondsBetween(start, countedTargets), + "ms\n"); + absl::StrAppend(&desc, "\tDetermined least recently used ", sequenceNumbers, + " sequence numbers in ", millisecondsBetween(countedTargets, foundUpperBound), + "ms\n"); + absl::StrAppend(&desc, "\tRemoved ", numTargetsRemoved, " targets in ", + millisecondsBetween(foundUpperBound, removedTargets), "ms\n"); + absl::StrAppend(&desc, "\tRemoved ", numDocumentsRemoved, " documents in ", + millisecondsBetween(removedTargets, removedDocuments), "ms\n"); + absl::StrAppend(&desc, "Total duration: ", millisecondsBetween(start, removedDocuments), "ms"); + LOG_DEBUG(desc.c_str()); + + return LruResults{/* didRun= */ true, sequenceNumbers, numTargetsRemoved, numDocumentsRemoved}; +} + - (int)queryCountForPercentile:(NSUInteger)percentile { - int totalCount = [self.queryCache count]; + int totalCount = [_delegate sequenceNumberCount]; int setSize = (int)((percentile / 100.0f) * totalCount); return setSize; } diff --git a/Firestore/Source/Local/FSTLevelDB.h b/Firestore/Source/Local/FSTLevelDB.h index 947397e19b7..3128587a463 100644 --- a/Firestore/Source/Local/FSTLevelDB.h +++ b/Firestore/Source/Local/FSTLevelDB.h @@ -20,6 +20,7 @@ #include #include +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTPersistence.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" @@ -31,6 +32,9 @@ NS_ASSUME_NONNULL_BEGIN +@interface FSTLevelDBLRUDelegate : NSObject +@end + /** A LevelDB-backed instance of FSTPersistence. */ // TODO(mikelehen): Rename to FSTLevelDBPersistence. @interface FSTLevelDB : NSObject @@ -40,7 +44,9 @@ NS_ASSUME_NONNULL_BEGIN * opening any database files is deferred until -[FSTPersistence start] is called. */ - (instancetype)initWithDirectory:(firebase::firestore::util::Path)directory - serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; + serializer:(FSTLocalSerializer *)serializer + lruParams:(firebase::firestore::local::LruParams)lruParams + NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; @@ -80,6 +86,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) const std::set &users; +@property(nonatomic, readonly, strong) FSTLevelDBLRUDelegate *referenceDelegate; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index ad5e9bc50db..3087d244b49 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -61,6 +61,7 @@ using firebase::firestore::local::LevelDbMigrations; using firebase::firestore::local::LevelDbMutationKey; using firebase::firestore::local::LevelDbTransaction; +using firebase::firestore::local::LruParams; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::ListenSequenceNumber; @@ -92,7 +93,7 @@ - (size_t)byteSize; * Although this could implement FSTTransactional, it doesn't because it is not directly tied to * a transaction runner, it just happens to be called from FSTLevelDB, which is FSTTransactional. */ -@interface FSTLevelDBLRUDelegate : NSObject +@interface FSTLevelDBLRUDelegate () - (void)transactionWillStart; @@ -112,10 +113,9 @@ @implementation FSTLevelDBLRUDelegate { FSTListenSequence *_listenSequence; } -- (instancetype)initWithPersistence:(FSTLevelDB *)persistence { +- (instancetype)initWithPersistence:(FSTLevelDB *)persistence lruParams:(LruParams)lruParams { if (self = [super init]) { - _gc = - [[FSTLRUGarbageCollector alloc] initWithQueryCache:[persistence queryCache] delegate:self]; + _gc = [[FSTLRUGarbageCollector alloc] initWithDelegate:self params:lruParams]; _db = persistence; _currentSequenceNumber = kFSTListenSequenceNumberInvalid; } @@ -229,6 +229,15 @@ - (int)removeTargetsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber return [queryCache removeQueriesThroughSequenceNumber:sequenceNumber liveQueries:liveQueries]; } +- (int32_t)sequenceNumberCount { + __block int32_t totalCount = [_db.queryCache count]; + [self enumerateMutationsUsingBlock:^(const DocumentKey &key, ListenSequenceNumber sequenceNumber, + BOOL *stop) { + totalCount++; + }]; + return totalCount; +} + - (FSTLRUGarbageCollector *)gc { return _gc; } @@ -290,12 +299,15 @@ + (const ReadOptions)standardReadOptions { return users; } -- (instancetype)initWithDirectory:(Path)directory serializer:(FSTLocalSerializer *)serializer { +- (instancetype)initWithDirectory:(firebase::firestore::util::Path)directory + serializer:(FSTLocalSerializer *)serializer + lruParams:(firebase::firestore::local::LruParams)lruParams { if (self = [super init]) { _directory = std::move(directory); _serializer = serializer; _queryCache = [[FSTLevelDBQueryCache alloc] initWithDB:self serializer:self.serializer]; - _referenceDelegate = [[FSTLevelDBLRUDelegate alloc] initWithPersistence:self]; + _referenceDelegate = + [[FSTLevelDBLRUDelegate alloc] initWithPersistence:self lruParams:lruParams]; _transactionRunner.SetBackingPersistence(self); } return self; diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h index 6978afa1250..0ea15110503 100644 --- a/Firestore/Source/Local/FSTLocalStore.h +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -16,6 +16,7 @@ #import +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" @@ -176,6 +177,8 @@ NS_ASSUME_NONNULL_BEGIN - (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID: (firebase::firestore::model::BatchId)batchID; +- (firebase::firestore::local::LruResults)collectGarbage:(FSTLRUGarbageCollector *)garbageCollector; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 4031013fcdd..2ed4b6832a0 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -21,6 +21,7 @@ #import "FIRTimestamp.h" #import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" #import "Firestore/Source/Local/FSTLocalWriteResult.h" @@ -45,6 +46,7 @@ using firebase::firestore::auth::User; using firebase::firestore::core::TargetIdGenerator; +using firebase::firestore::local::LruResults; using firebase::firestore::model::BatchId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -459,6 +461,12 @@ - (void)applyBatchResult:(FSTMutationBatchResult *)batchResult { [self.mutationQueue removeMutationBatch:batch]; } +- (LruResults)collectGarbage:(FSTLRUGarbageCollector *)garbageCollector { + return self.persistence.run("Collect garbage", [&]() -> LruResults { + return [garbageCollector collectWithLiveTargets:_targetIDs]; + }); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm index 3109bdfa6cf..4648a78f733 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.mm @@ -384,7 +384,7 @@ - (NSUInteger)indexOfExistingBatchID:(BatchId)batchID action:(NSString *)action - (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer { size_t count = 0; for (FSTMutationBatch *batch in self.queue) { - count += [[[serializer encodedMutationBatch:batch] data] length]; + count += [[serializer encodedMutationBatch:batch] serializedSize]; }; return count; } diff --git a/Firestore/Source/Local/FSTMemoryPersistence.h b/Firestore/Source/Local/FSTMemoryPersistence.h index 94ee875f155..c2938435dfa 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.h +++ b/Firestore/Source/Local/FSTMemoryPersistence.h @@ -32,7 +32,8 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)persistenceWithEagerGC; -+ (instancetype)persistenceWithLRUGCAndSerializer:(FSTLocalSerializer *)serializer; ++ (instancetype)persistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams + serializer:(FSTLocalSerializer *)serializer; @end @@ -52,7 +53,8 @@ NS_ASSUME_NONNULL_BEGIN : NSObject - (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence - serializer:(FSTLocalSerializer *)serializer; + serializer:(FSTLocalSerializer *)serializer + lruParams:(firebase::firestore::local::LruParams)lruParams; - (BOOL)isPinnedAtSequenceNumber:(firebase::firestore::model::ListenSequenceNumber)upperBound document:(const firebase::firestore::model::DocumentKey &)key; diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm index b4c4effbb72..e639ecc0de0 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -34,6 +34,7 @@ using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; +using firebase::firestore::local::LruParams; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeyHash; using firebase::firestore::model::ListenSequenceNumber; @@ -84,10 +85,13 @@ + (instancetype)persistenceWithEagerGC { return persistence; } -+ (instancetype)persistenceWithLRUGCAndSerializer:(FSTLocalSerializer *)serializer { ++ (instancetype)persistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams + serializer:(FSTLocalSerializer *)serializer { FSTMemoryPersistence *persistence = [[FSTMemoryPersistence alloc] init]; persistence.referenceDelegate = - [[FSTMemoryLRUReferenceDelegate alloc] initWithPersistence:persistence serializer:serializer]; + [[FSTMemoryLRUReferenceDelegate alloc] initWithPersistence:persistence + serializer:serializer + lruParams:lruParams]; return persistence; } @@ -166,11 +170,11 @@ @implementation FSTMemoryLRUReferenceDelegate { } - (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence - serializer:(FSTLocalSerializer *)serializer { + serializer:(FSTLocalSerializer *)serializer + lruParams:(firebase::firestore::local::LruParams)lruParams { if (self = [super init]) { _persistence = persistence; - _gc = - [[FSTLRUGarbageCollector alloc] initWithQueryCache:[_persistence queryCache] delegate:self]; + _gc = [[FSTLRUGarbageCollector alloc] initWithDelegate:self params:lruParams]; _currentSequenceNumber = kFSTListenSequenceNumberInvalid; // Theoretically this is always 0, since this is all in-memory... ListenSequenceNumber highestSequenceNumber = @@ -226,7 +230,9 @@ - (void)enumerateMutationsUsingBlock: for (const auto &entry : _sequenceNumbers) { ListenSequenceNumber sequenceNumber = entry.second; const DocumentKey &key = entry.first; - if (![_persistence.queryCache containsKey:key]) { + // Pass in the exact sequence number as the upper bound so we know it won't be pinned by being + // too recent. + if (![self isPinnedAtSequenceNumber:sequenceNumber document:key]) { block(key, sequenceNumber, &stop); } } @@ -238,6 +244,15 @@ - (int)removeTargetsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber liveQueries:liveQueries]; } +- (int32_t)sequenceNumberCount { + __block int32_t totalCount = [_persistence.queryCache count]; + [self enumerateMutationsUsingBlock:^(const DocumentKey &key, ListenSequenceNumber sequenceNumber, + BOOL *stop) { + totalCount++; + }]; + return totalCount; +} + - (int)removeOrphanedDocumentsThroughSequenceNumber:(ListenSequenceNumber)upperBound { std::vector removed = [(FSTMemoryRemoteDocumentCache *)_persistence.remoteDocumentCache diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.mm b/Firestore/Source/Local/FSTMemoryQueryCache.mm index 16d4055d213..44bf33f7458 100644 --- a/Firestore/Source/Local/FSTMemoryQueryCache.mm +++ b/Firestore/Source/Local/FSTMemoryQueryCache.mm @@ -175,7 +175,7 @@ - (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer { __block size_t count = 0; [self.queries enumerateKeysAndObjectsUsingBlock:^(FSTQuery *key, FSTQueryData *queryData, BOOL *stop) { - count += [[[serializer encodedQueryData:queryData] data] length]; + count += [[serializer encodedQueryData:queryData] serializedSize]; }]; return count; } diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm index 889268822f5..6a6556b575c 100644 --- a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm +++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.mm @@ -115,7 +115,7 @@ - (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer { [self.docs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *doc, BOOL *stop) { count += FSTDocumentKeyByteSize(key); - count += [[[serializer encodedMaybeDocument:doc] data] length]; + count += [[serializer encodedMaybeDocument:doc] serializedSize]; }]; return count; } diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h index 088d43d465a..6d481b972ad 100644 --- a/Firestore/Source/Local/FSTPersistence.h +++ b/Firestore/Source/Local/FSTPersistence.h @@ -124,7 +124,7 @@ NS_ASSUME_NONNULL_BEGIN * Implementations that care about sequence numbers are responsible for generating them and making * them available. */ -@protocol FSTReferenceDelegate +@protocol FSTReferenceDelegate /** * Registers an FSTReferenceSet of documents that should be considered 'referenced' and not eligible diff --git a/Firestore/Source/Public/FIRFirestoreSettings.h b/Firestore/Source/Public/FIRFirestoreSettings.h index cd3f91ced57..1e82134acdb 100644 --- a/Firestore/Source/Public/FIRFirestoreSettings.h +++ b/Firestore/Source/Public/FIRFirestoreSettings.h @@ -18,6 +18,9 @@ NS_ASSUME_NONNULL_BEGIN +/** Used to set on-disk cache size to unlimited. Garbage collection will not run. */ +extern const int64_t kFIRFirestoreCacheSizeUnlimited NS_SWIFT_NAME(FirestoreCacheSizeUnlimited); + /** Settings used to configure a `FIRFirestore` instance. */ NS_SWIFT_NAME(FirestoreSettings) @interface FIRFirestoreSettings : NSObject @@ -61,6 +64,15 @@ NS_SWIFT_NAME(FirestoreSettings) */ @property(nonatomic, getter=areTimestampsInSnapshotsEnabled) BOOL timestampsInSnapshotsEnabled; +/** + * Sets the cache size threshold above which the SDK will attempt to collect least-recently-used + * documents. The size is not a guarantee that the cache will stay below that size, only that if + * the cache exceeds the given size, cleanup will be attempted. Cannot be set lower than 1MB. + * + * Set to kFIRFirestoreCacheSizeUnlimited to disable garbage collection entirely. + */ +@property(nonatomic, assign) int64_t cacheSizeBytes; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Swift/Tests/API/BasicCompileTests.swift b/Firestore/Swift/Tests/API/BasicCompileTests.swift index 16886e22955..2191de2cf2d 100644 --- a/Firestore/Swift/Tests/API/BasicCompileTests.swift +++ b/Firestore/Swift/Tests/API/BasicCompileTests.swift @@ -64,6 +64,7 @@ func initializeDb() -> Firestore { settings.host = "localhost" settings.isPersistenceEnabled = true settings.areTimestampsInSnapshotsEnabled = true + settings.cacheSizeBytes = FirestoreCacheSizeUnlimited firestore.settings = settings return firestore diff --git a/Firestore/core/src/firebase/firestore/util/async_queue.h b/Firestore/core/src/firebase/firestore/util/async_queue.h index 0194e76a575..e3971eed380 100644 --- a/Firestore/core/src/firebase/firestore/util/async_queue.h +++ b/Firestore/core/src/firebase/firestore/util/async_queue.h @@ -54,6 +54,10 @@ enum class TimerId { * indefinitely for success or failure. */ OnlineStateTimeout, + /** + * A timer used to periodically attempt LRU Garbage collection + */ + GarbageCollectionDelay }; // A serial queue that executes given operations asynchronously, one at a time.