Skip to content

Commit 4da49cf

Browse files
Decode documents in background thread
1 parent 2e162e3 commit 4da49cf

File tree

7 files changed

+102
-19
lines changed

7 files changed

+102
-19
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Unreleased
2+
- [changed] Reduced execution time of queries with large result sets by up to
3+
60%.
24
- [changed] Instead of failing silently, Firestore now crashes the client app
35
if it fails to load SSL Ciphers. To avoid these crashes, you must bundle
46
Conscrypt to support non-GMSCore devices on Android KitKat or JellyBean (see

firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalSerializer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
import java.util.List;
3636
import java.util.Map;
3737

38-
/** Serializer for values stored in the LocalStore. */
38+
/**
39+
* Serializer for values stored in the LocalStore.
40+
*
41+
* <p>This class is thread-safe.
42+
*/
3943
public final class LocalSerializer {
4044

4145
private final RemoteSerializer rpcSerializer;

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@
2323
import com.google.firebase.firestore.model.DocumentKey;
2424
import com.google.firebase.firestore.model.MaybeDocument;
2525
import com.google.firebase.firestore.model.ResourcePath;
26+
import com.google.firebase.firestore.util.Executors;
2627
import com.google.protobuf.InvalidProtocolBufferException;
2728
import com.google.protobuf.MessageLite;
2829
import java.util.ArrayList;
2930
import java.util.HashMap;
3031
import java.util.List;
3132
import java.util.Map;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import java.util.concurrent.Executor;
35+
import java.util.concurrent.Semaphore;
3236
import javax.annotation.Nullable;
3337

3438
final class SQLiteRemoteDocumentCache implements RemoteDocumentCache {
@@ -118,7 +122,10 @@ public ImmutableSortedMap<DocumentKey, Document> getAllDocumentsMatchingQuery(Qu
118122
String prefixPath = EncodedPath.encode(prefix);
119123
String prefixSuccessorPath = EncodedPath.prefixSuccessor(prefixPath);
120124

121-
Map<DocumentKey, Document> results = new HashMap<>();
125+
Map<DocumentKey, Document> allDocuments = new ConcurrentHashMap<>();
126+
127+
int[] pendingTaskCount = new int[] {0};
128+
Semaphore completedTasks = new Semaphore(0);
122129

123130
db.query("SELECT path, contents FROM remote_documents WHERE path >= ? AND path < ?")
124131
.binding(prefixPath, prefixSuccessorPath)
@@ -136,20 +143,38 @@ public ImmutableSortedMap<DocumentKey, Document> getAllDocumentsMatchingQuery(Qu
136143
return;
137144
}
138145

139-
MaybeDocument maybeDoc = decodeMaybeDocument(row.getBlob(1));
140-
if (!(maybeDoc instanceof Document)) {
141-
return;
142-
}
143-
144-
Document doc = (Document) maybeDoc;
145-
if (!query.matches(doc)) {
146-
return;
147-
}
148-
149-
results.put(doc.getKey(), doc);
146+
byte[] rawContents = row.getBlob(1);
147+
148+
++pendingTaskCount[0];
149+
150+
// Since scheduling background tasks incurs overhead, we only dispatch to a background
151+
// thread if there are still some documents remaining.
152+
Executor deserializationExecutor =
153+
row.isLast() ? Executors.DIRECT_EXECUTOR : Executors.BACKGROUND_EXECUTOR;
154+
deserializationExecutor.execute(
155+
() -> {
156+
MaybeDocument maybeDoc = decodeMaybeDocument(rawContents);
157+
if (maybeDoc instanceof Document) {
158+
allDocuments.put(maybeDoc.getKey(), (Document) maybeDoc);
159+
}
160+
completedTasks.release();
161+
});
150162
});
151163

152-
return ImmutableSortedMap.Builder.fromMap(results, DocumentKey.comparator());
164+
try {
165+
completedTasks.acquire(pendingTaskCount[0]);
166+
} catch (InterruptedException e) {
167+
Thread.currentThread().interrupt();
168+
}
169+
170+
ImmutableSortedMap<DocumentKey, Document> matchingDocuments =
171+
ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator());
172+
for (Map.Entry<DocumentKey, Document> entry : allDocuments.entrySet()) {
173+
if (query.matches(entry.getValue())) {
174+
matchingDocuments = matchingDocuments.insert(entry.getKey(), entry.getValue());
175+
}
176+
}
177+
return matchingDocuments;
153178
}
154179

155180
private String pathForKey(DocumentKey key) {

firebase-firestore/src/main/java/com/google/firebase/firestore/model/DatabaseId.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
import androidx.annotation.NonNull;
1818

19-
/** Represents a particular database in Firestore */
19+
/**
20+
* Represents a particular database in Firestore.
21+
*
22+
* <p>This class is thread-safe.
23+
*/
2024
public final class DatabaseId implements Comparable<DatabaseId> {
2125
public static final String DEFAULT_DATABASE_ID = "(default)";
2226

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/GrpcCallProvider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package com.google.firebase.firestore.remote;
1616

1717
import android.content.Context;
18-
import android.os.AsyncTask;
1918
import androidx.annotation.VisibleForTesting;
2019
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
2120
import com.google.android.gms.common.GooglePlayServicesRepairableException;
@@ -24,6 +23,7 @@
2423
import com.google.android.gms.tasks.Tasks;
2524
import com.google.firebase.firestore.core.DatabaseInfo;
2625
import com.google.firebase.firestore.util.AsyncQueue;
26+
import com.google.firebase.firestore.util.Executors;
2727
import com.google.firebase.firestore.util.Logger;
2828
import com.google.firebase.firestore.util.Supplier;
2929
import com.google.firestore.v1.FirestoreGrpc;
@@ -69,11 +69,11 @@ public static void overrideChannelBuilder(
6969
CallCredentials firestoreHeaders) {
7070
this.asyncQueue = asyncQueue;
7171

72-
// We execute network initialization on a separate thred to not block operations that depend on
72+
// We execute network initialization on a separate thread to not block operations that depend on
7373
// the AsyncQueue.
7474
this.channelTask =
7575
Tasks.call(
76-
AsyncTask.THREAD_POOL_EXECUTOR,
76+
Executors.BACKGROUND_EXECUTOR,
7777
() -> {
7878
ManagedChannel channel = initChannel(context, databaseInfo);
7979
FirestoreGrpc.FirestoreStub firestoreStub =

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@
105105
import java.util.Map;
106106
import java.util.Set;
107107

108-
/** Serializer that converts to and from Firestore API protos. */
108+
/**
109+
* Serializer that converts to and from Firestore API protos.
110+
*
111+
* <p>This class is thread-safe.
112+
*/
109113
public final class RemoteSerializer {
110114

111115
private final DatabaseId databaseId;

firebase-firestore/src/main/java/com/google/firebase/firestore/util/Executors.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,22 @@
1414

1515
package com.google.firebase.firestore.util;
1616

17+
import android.os.AsyncTask;
1718
import com.google.android.gms.tasks.TaskExecutors;
19+
import java.util.Queue;
20+
import java.util.concurrent.ConcurrentLinkedQueue;
1821
import java.util.concurrent.Executor;
22+
import java.util.concurrent.atomic.AtomicInteger;
1923

2024
/** Helper class for executors. */
2125
public final class Executors {
26+
/**
27+
* The maximum number of tasks we submit to AsyncTask.THREAD_POOL_EXECUTOR.
28+
*
29+
* <p>The limit is based on the number of core threads spun by THREAD_POOL_EXECUTOR and addresses
30+
* its queue size limit of 120 pending tasks.
31+
*/
32+
private static final int MAXIMUM_CONCURRENT_BACKGROUND_TASKS = 4;
2233

2334
/**
2435
* The default executor for user visible callbacks. It is an executor scheduling callbacks on
@@ -29,6 +40,39 @@ public final class Executors {
2940
/** An executor that executes the provided runnable immediately on the current thread. */
3041
public static final Executor DIRECT_EXECUTOR = Runnable::run;
3142

43+
/**
44+
* An executor that runs tasks in parallel on Android's AsyncTask.THREAD_POOL_EXECUTOR.
45+
*
46+
* <p>Unlike the main THREAD_POOL_EXECUTOR, this executor manages its own queue of tasks and can
47+
* handle an unbounded number of pending tasks.
48+
*/
49+
public static final Executor BACKGROUND_EXECUTOR =
50+
new Executor() {
51+
AtomicInteger activeRunnerCount = new AtomicInteger(0);
52+
Queue<Runnable> pendingTasks = new ConcurrentLinkedQueue<>();
53+
54+
@Override
55+
public void execute(Runnable command) {
56+
pendingTasks.add(command);
57+
58+
if (activeRunnerCount.get() < MAXIMUM_CONCURRENT_BACKGROUND_TASKS) {
59+
// Note that the runner count could temporarily exceed
60+
// MAXIMUM_CONCURRENT_BACKGROUND_TASKS if this is code path was executed in parallel.
61+
// While undesired, this would merely queue another task on THREAD_POOL_EXECUTOR,
62+
// and we are unlikely to hit the 120 pending task limit.
63+
activeRunnerCount.incrementAndGet();
64+
AsyncTask.THREAD_POOL_EXECUTOR.execute(
65+
() -> {
66+
Runnable r;
67+
while ((r = pendingTasks.poll()) != null) {
68+
r.run();
69+
}
70+
activeRunnerCount.decrementAndGet();
71+
});
72+
}
73+
}
74+
};
75+
3276
private Executors() {
3377
// Private constructor to prevent initialization
3478
}

0 commit comments

Comments
 (0)