Skip to content

Enable LRU GC #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Dec 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,7 @@ private void ensureClientConfigured() {
new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled());

client =
new FirestoreClient(
context,
databaseInfo,
settings.isPersistenceEnabled(),
credentialsProvider,
asyncQueue);
new FirestoreClient(context, databaseInfo, settings, credentialsProvider, asyncQueue);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@
/** Settings used to configure a FirebaseFirestore instance. */
@PublicApi
public final class FirebaseFirestoreSettings {
/**
* Constant to use with {@link FirebaseFirestoreSettings.Builder#setCacheSizeBytes(long)} to
* disable garbage collection.
*/
@PublicApi public static final long CACHE_SIZE_UNLIMITED = -1;

private static final long MINIMUM_CACHE_BYTES = 1 * 1024 * 1024; // 1 MB
// TODO(b/121269744): Set this to be the default value after SDK is past version 1.0
// private static final long DEFAULT_CACHE_SIZE_BYTES = 100 * 1024 * 1024; // 100 MB
// For now, we are rolling this out with collection disabled. Once the SDK has hit version 1.0,
// we will switch the default to the above value, 100 MB.
private static final long DEFAULT_CACHE_SIZE_BYTES = CACHE_SIZE_UNLIMITED;
private static final String DEFAULT_HOST = "firestore.googleapis.com";
private static final boolean DEFAULT_TIMESTAMPS_IN_SNAPSHOTS_ENABLED = false;

Expand All @@ -34,6 +46,7 @@ public static final class Builder {
private boolean sslEnabled;
private boolean persistenceEnabled;
private boolean timestampsInSnapshotsEnabled;
private long cacheSizeBytes;

/** Constructs a new FirebaseFirestoreSettings Builder object. */
@PublicApi
Expand All @@ -42,6 +55,7 @@ public Builder() {
sslEnabled = true;
persistenceEnabled = true;
timestampsInSnapshotsEnabled = DEFAULT_TIMESTAMPS_IN_SNAPSHOTS_ENABLED;
cacheSizeBytes = DEFAULT_CACHE_SIZE_BYTES;
}

/**
Expand Down Expand Up @@ -124,6 +138,30 @@ public Builder setTimestampsInSnapshotsEnabled(boolean value) {
return this;
}

/**
* Sets an approximate cache size threshold for the on-disk data. If the cache grows beyond this
* size, Firestore will start removing data that hasn't been recently used. 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.
*
* <p>By default, collection is disabled (the value is set to {@link
* FirebaseFirestoreSettings#CACHE_SIZE_UNLIMITED}). In a future release, collection will be
* enabled by default, with a default cache size of 100 MB. The minimum value is 1 MB.
*
* @return A settings object on which the cache size is configured as specified by the given
* {@code value}.
*/
@NonNull
@PublicApi
public Builder setCacheSizeBytes(long value) {
if (value != CACHE_SIZE_UNLIMITED && value < MINIMUM_CACHE_BYTES) {
throw new IllegalArgumentException(
"Cache size must be set to at least " + MINIMUM_CACHE_BYTES + " bytes");
}
this.cacheSizeBytes = value;
return this;
}

@NonNull
@PublicApi
public FirebaseFirestoreSettings build() {
Expand All @@ -139,13 +177,15 @@ public FirebaseFirestoreSettings build() {
private final boolean sslEnabled;
private final boolean persistenceEnabled;
private final boolean timestampsInSnapshotsEnabled;
private final long cacheSizeBytes;

/** Constructs a FirebaseFirestoreSettings object based on the values in the Builder. */
private FirebaseFirestoreSettings(Builder builder) {
host = builder.host;
sslEnabled = builder.sslEnabled;
persistenceEnabled = builder.persistenceEnabled;
timestampsInSnapshotsEnabled = builder.timestampsInSnapshotsEnabled;
cacheSizeBytes = builder.cacheSizeBytes;
}

@Override
Expand All @@ -161,7 +201,8 @@ public boolean equals(@Nullable Object o) {
return host.equals(that.host)
&& sslEnabled == that.sslEnabled
&& persistenceEnabled == that.persistenceEnabled
&& timestampsInSnapshotsEnabled == that.timestampsInSnapshotsEnabled;
&& timestampsInSnapshotsEnabled == that.timestampsInSnapshotsEnabled
&& cacheSizeBytes == that.cacheSizeBytes;
}

@Override
Expand All @@ -170,6 +211,7 @@ public int hashCode() {
result = 31 * result + (sslEnabled ? 1 : 0);
result = 31 * result + (persistenceEnabled ? 1 : 0);
result = 31 * result + (timestampsInSnapshotsEnabled ? 1 : 0);
result = 31 * result + (int) cacheSizeBytes;
return result;
}

Expand Down Expand Up @@ -211,4 +253,13 @@ public boolean isPersistenceEnabled() {
public boolean areTimestampsInSnapshotsEnabled() {
return timestampsInSnapshotsEnabled;
}

/**
* Returns the threshold for the cache size above which the SDK will attempt to collect the least
* recently used documents.
*/
@PublicApi
public long getCacheSizeBytes() {
return cacheSizeBytes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
import com.google.firebase.firestore.FirebaseFirestoreSettings;
import com.google.firebase.firestore.auth.CredentialsProvider;
import com.google.firebase.firestore.auth.User;
import com.google.firebase.firestore.core.EventManager.ListenOptions;
import com.google.firebase.firestore.local.LocalSerializer;
import com.google.firebase.firestore.local.LocalStore;
import com.google.firebase.firestore.local.LruDelegate;
import com.google.firebase.firestore.local.LruGarbageCollector;
import com.google.firebase.firestore.local.MemoryPersistence;
import com.google.firebase.firestore.local.Persistence;
import com.google.firebase.firestore.local.SQLitePersistence;
Expand Down Expand Up @@ -71,10 +74,13 @@ public final class FirestoreClient implements RemoteStore.RemoteStoreCallback {
private SyncEngine syncEngine;
private EventManager eventManager;

// LRU-related
@Nullable private LruGarbageCollector.Scheduler lruScheduler;

public FirestoreClient(
final Context context,
DatabaseInfo databaseInfo,
final boolean usePersistence,
FirebaseFirestoreSettings settings,
CredentialsProvider credentialsProvider,
final AsyncQueue asyncQueue) {
this.databaseInfo = databaseInfo;
Expand Down Expand Up @@ -105,7 +111,11 @@ public FirestoreClient(
try {
// Block on initial user being available
User initialUser = Tasks.await(firstUser.getTask());
initialize(context, initialUser, usePersistence);
initialize(
context,
initialUser,
settings.isPersistenceEnabled(),
settings.getCacheSizeBytes());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
Expand All @@ -127,6 +137,9 @@ public Task<Void> shutdown() {
() -> {
remoteStore.shutdown();
persistence.shutdown();
if (lruScheduler != null) {
lruScheduler.stop();
}
});
}

Expand Down Expand Up @@ -194,24 +207,38 @@ public <TResult> Task<TResult> transaction(
() -> syncEngine.transaction(asyncQueue, updateFunction, retries));
}

private void initialize(Context context, User user, boolean usePersistence) {
private void initialize(Context context, User user, boolean usePersistence, long cacheSizeBytes) {
// 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.
Logger.debug(LOG_TAG, "Initializing. user=%s", user.getUid());

LruGarbageCollector gc = null;
if (usePersistence) {
LocalSerializer serializer =
new LocalSerializer(new RemoteSerializer(databaseInfo.getDatabaseId()));
persistence =
LruGarbageCollector.Params params =
LruGarbageCollector.Params.WithCacheSizeBytes(cacheSizeBytes);
SQLitePersistence sqlitePersistence =
new SQLitePersistence(
context, databaseInfo.getPersistenceKey(), databaseInfo.getDatabaseId(), serializer);
context,
databaseInfo.getPersistenceKey(),
databaseInfo.getDatabaseId(),
serializer,
params);
LruDelegate lruDelegate = sqlitePersistence.getReferenceDelegate();
gc = lruDelegate.getGarbageCollector();
persistence = sqlitePersistence;
} else {
persistence = MemoryPersistence.createEagerGcMemoryPersistence();
}

persistence.start();
localStore = new LocalStore(persistence, user);
if (gc != null) {
lruScheduler = gc.newScheduler(asyncQueue, localStore);
lruScheduler.start();
}

Datastore datastore = new Datastore(databaseInfo, asyncQueue, credentialsProvider, context);
remoteStore = new RemoteStore(this, localStore, datastore, asyncQueue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,4 +570,8 @@ private void applyWriteToRemoteDocuments(MutationBatchResult batchResult) {

mutationQueue.removeMutationBatch(batch);
}

public LruGarbageCollector.Results collectGarbage(LruGarbageCollector garbageCollector) {
return persistence.runTransaction("Collect garbage", () -> garbageCollector.collect(targetIds));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
* Persistence layers intending to use LRU Garbage collection should implement this interface. This
* interface defines the operations that the LRU garbage collector needs from the persistence layer.
*/
interface LruDelegate {
public interface LruDelegate {

/** Enumerates all the targets in the QueryCache. */
void forEachTarget(Consumer<QueryData> consumer);

long getTargetCount();
long getSequenceNumberCount();

/** Enumerates sequence numbers for documents not associated with a target. */
void forEachOrphanedDocumentSequenceNumber(Consumer<Long> consumer);
Expand All @@ -49,4 +49,7 @@ interface LruDelegate {

/** Access to the underlying LRU Garbage collector instance. */
LruGarbageCollector getGarbageCollector();

/** Return the size of the cache in bytes. */
long getByteSize();
}
Loading