Skip to content

Commit 0dca5d0

Browse files
committed
BackgroundQueueNoMoreAsyncTaskThreadPool #7376
1 parent 2083b52 commit 0dca5d0

File tree

6 files changed

+110
-74
lines changed

6 files changed

+110
-74
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
- [changed] Bumped internal dependencies.
44
- [changed] Improve the performance of queries in collections that contain many deleted documents.
55
[#7295](//github.com/firebase/firebase-android-sdk/issues/7295)
6+
- [changed] Improve query performance in large result sets by replacing the deprecated AsyncTask
7+
thread pool with a self-managed thread pool.
8+
[#7376](//github.com/firebase/firebase-android-sdk/issues/7376)
69

710
# 26.0.0
811

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import com.google.firebase.firestore.model.mutation.Mutation;
2727
import com.google.firebase.firestore.model.mutation.Overlay;
2828
import com.google.firebase.firestore.util.BackgroundQueue;
29-
import com.google.firebase.firestore.util.Executors;
3029
import com.google.firestore.v1.Write;
3130
import com.google.protobuf.InvalidProtocolBufferException;
3231
import java.util.ArrayList;
@@ -35,7 +34,6 @@
3534
import java.util.List;
3635
import java.util.Map;
3736
import java.util.SortedSet;
38-
import java.util.concurrent.Executor;
3937

4038
public class SQLiteDocumentOverlayCache implements DocumentOverlayCache {
4139
private final SQLitePersistence db;
@@ -204,16 +202,21 @@ private void processOverlaysInBackground(
204202
byte[] rawMutation = row.getBlob(0);
205203
int largestBatchId = row.getInt(1);
206204

207-
// Since scheduling background tasks incurs overhead, we only dispatch to a
208-
// background thread if there are still some documents remaining.
209-
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
210-
executor.execute(
205+
Runnable runnable =
211206
() -> {
212207
Overlay overlay = decodeOverlay(rawMutation, largestBatchId);
213208
synchronized (results) {
214209
results.put(overlay.getKey(), overlay);
215210
}
216-
});
211+
};
212+
213+
// If the cursor has exactly one row then just process that row synchronously to avoid the
214+
// unnecessary overhead of scheduling its processing to run asynchronously.
215+
if (row.isFirst() && row.isLast()) {
216+
runnable.run();
217+
} else {
218+
backgroundQueue.submit(runnable);
219+
}
217220
}
218221

219222
private Overlay decodeOverlay(byte[] rawMutation, int largestBatchId) {

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import com.google.firebase.firestore.model.ResourcePath;
3434
import com.google.firebase.firestore.model.SnapshotVersion;
3535
import com.google.firebase.firestore.util.BackgroundQueue;
36-
import com.google.firebase.firestore.util.Executors;
3736
import com.google.firebase.firestore.util.Function;
3837
import com.google.protobuf.InvalidProtocolBufferException;
3938
import com.google.protobuf.MessageLite;
@@ -47,7 +46,6 @@
4746
import java.util.Objects;
4847
import java.util.Set;
4948
import java.util.concurrent.ConcurrentHashMap;
50-
import java.util.concurrent.Executor;
5149
import javax.annotation.Nonnull;
5250
import javax.annotation.Nullable;
5351

@@ -308,10 +306,7 @@ private void processRowInBackground(
308306
boolean documentTypeIsNull = row.isNull(3);
309307
String path = row.getString(4);
310308

311-
// Since scheduling background tasks incurs overhead, we only dispatch to a
312-
// background thread if there are still some documents remaining.
313-
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
314-
executor.execute(
309+
Runnable runnable =
315310
() -> {
316311
MutableDocument document =
317312
decodeMaybeDocument(rawDocument, readTimeSeconds, readTimeNanos);
@@ -323,7 +318,15 @@ private void processRowInBackground(
323318
results.put(document.getKey(), document);
324319
}
325320
}
326-
});
321+
};
322+
323+
// If the cursor has exactly one row then just process that row synchronously to avoid the
324+
// unnecessary overhead of scheduling its processing to run asynchronously.
325+
if (row.isFirst() && row.isLast()) {
326+
runnable.run();
327+
} else {
328+
backgroundQueue.submit(runnable);
329+
}
327330
}
328331

329332
@Override

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

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.firebase.firestore.util
17+
18+
import java.util.concurrent.Executor
19+
import java.util.concurrent.Semaphore
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.asExecutor
22+
23+
/**
24+
* Manages CPU-bound work on background threads to enable parallel processing.
25+
*
26+
* Instances of this class are _not_ thread-safe. All methods of an instance of this class must be
27+
* called from the same thread. The behavior of an instance is undefined if any of the methods are
28+
* called from multiple threads.
29+
*/
30+
internal class BackgroundQueue {
31+
32+
private var state: State = State.Submitting()
33+
34+
/**
35+
* Submit a task for asynchronous execution on the executor of the owning [BackgroundQueue].
36+
*
37+
* @throws IllegalStateException if [drain] has been called.
38+
*/
39+
fun submit(runnable: Runnable) {
40+
val submittingState = this.state
41+
check(submittingState is State.Submitting) { "submit() may not be called after drain()" }
42+
43+
submittingState.taskCount++
44+
executor.execute {
45+
try {
46+
runnable.run()
47+
} finally {
48+
submittingState.completedTasks.release()
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Blocks until all tasks submitted via calls to [submit] have completed.
55+
*
56+
* @throws IllegalStateException if called more than once.
57+
*/
58+
fun drain() {
59+
val submittingState = this.state
60+
check(submittingState is State.Submitting) { "drain() may not be called more than once" }
61+
this.state = State.Draining
62+
63+
submittingState.completedTasks.acquire(submittingState.taskCount)
64+
}
65+
66+
private sealed interface State {
67+
class Submitting : State {
68+
val completedTasks = Semaphore(0)
69+
var taskCount: Int = 0
70+
}
71+
object Draining : State
72+
}
73+
74+
companion object {
75+
76+
/**
77+
* The maximum amount of parallelism shared by all instances of this class.
78+
*
79+
* This is equal to the number of processor cores available, or 2, whichever is larger.
80+
*/
81+
val maxParallelism = Runtime.getRuntime().availableProcessors().coerceAtLeast(2)
82+
83+
private val executor: Executor =
84+
Dispatchers.IO.limitedParallelism(maxParallelism, "firestore.BackgroundQueue").asExecutor()
85+
}
86+
}

firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,12 @@
4040
import com.google.firebase.firestore.model.ResourcePath;
4141
import com.google.firebase.firestore.testutil.ComparatorTester;
4242
import com.google.firebase.firestore.util.BackgroundQueue;
43-
import com.google.firebase.firestore.util.Executors;
4443
import java.util.ArrayList;
4544
import java.util.Arrays;
4645
import java.util.HashMap;
4746
import java.util.Iterator;
4847
import java.util.List;
4948
import java.util.Map;
50-
import java.util.concurrent.Executor;
5149
import org.junit.Assert;
5250
import org.junit.Test;
5351
import org.junit.runner.RunWith;
@@ -991,10 +989,7 @@ public void testSynchronousMatchesOrderBy() {
991989

992990
while (iterator.hasNext()) {
993991
MutableDocument doc = iterator.next();
994-
// Only put the processing in the backgroundQueue if there are more documents
995-
// in the list. This behavior matches SQLiteRemoteDocumentCache.getAll(...)
996-
Executor executor = iterator.hasNext() ? backgroundQueue : Executors.DIRECT_EXECUTOR;
997-
executor.execute(
992+
backgroundQueue.submit(
998993
() -> {
999994
// We call query.matches() to indirectly test query.matchesOrderBy()
1000995
boolean result = query.matches(doc);

0 commit comments

Comments
 (0)