Skip to content

Commit 153d70a

Browse files
authored
Merge 42e41ac into 6349a09
2 parents 6349a09 + 42e41ac commit 153d70a

File tree

2 files changed

+319
-0
lines changed

2 files changed

+319
-0
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

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

17+
import static com.google.firebase.firestore.util.Assert.fail;
1718
import static com.google.firebase.firestore.util.Assert.hardAssert;
1819

1920
import androidx.annotation.VisibleForTesting;
@@ -116,6 +117,24 @@ public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
116117
return result;
117118
}
118119

120+
// Used for auto indexing experiment, allows test running specifically with or without field
121+
// indexes
122+
public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQueryForTest(
123+
Query query, boolean usingIndex, QueryContext counter) {
124+
hardAssert(initialized, "initialize() not called");
125+
126+
ImmutableSortedMap<DocumentKey, Document> result;
127+
if (usingIndex) {
128+
result = performQueryUsingIndex(query);
129+
if (result == null) {
130+
fail("createTargetIndices fails");
131+
}
132+
} else {
133+
result = executeFullCollectionScan(query, counter);
134+
}
135+
return result;
136+
}
137+
119138
/**
120139
* Decides whether SDK should create a full matched field index for this query based on query
121140
* context and query result size.
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.local;
16+
17+
import static com.google.firebase.firestore.testutil.TestUtil.doc;
18+
import static com.google.firebase.firestore.testutil.TestUtil.docMap;
19+
import static com.google.firebase.firestore.testutil.TestUtil.filter;
20+
import static com.google.firebase.firestore.testutil.TestUtil.map;
21+
import static com.google.firebase.firestore.testutil.TestUtil.patchMutation;
22+
import static com.google.firebase.firestore.testutil.TestUtil.query;
23+
import static org.junit.Assert.assertEquals;
24+
25+
import com.google.android.gms.common.internal.Preconditions;
26+
import com.google.firebase.Timestamp;
27+
import com.google.firebase.database.collection.ImmutableSortedMap;
28+
import com.google.firebase.database.collection.ImmutableSortedSet;
29+
import com.google.firebase.firestore.auth.User;
30+
import com.google.firebase.firestore.core.Query;
31+
import com.google.firebase.firestore.core.View;
32+
import com.google.firebase.firestore.model.Document;
33+
import com.google.firebase.firestore.model.DocumentKey;
34+
import com.google.firebase.firestore.model.DocumentSet;
35+
import com.google.firebase.firestore.model.FieldIndex;
36+
import com.google.firebase.firestore.model.MutableDocument;
37+
import com.google.firebase.firestore.model.mutation.Mutation;
38+
import com.google.firebase.firestore.model.mutation.MutationBatch;
39+
import java.util.ArrayList;
40+
import java.util.Arrays;
41+
import java.util.Collections;
42+
import java.util.List;
43+
import java.util.Map;
44+
import java.util.concurrent.Callable;
45+
import java.util.concurrent.TimeUnit;
46+
import javax.annotation.Nullable;
47+
import org.junit.Before;
48+
import org.junit.Test;
49+
import org.junit.runner.RunWith;
50+
import org.robolectric.RobolectricTestRunner;
51+
import org.robolectric.annotation.Config;
52+
53+
@RunWith(RobolectricTestRunner.class)
54+
@Config(manifest = Config.NONE)
55+
public class AutoIndexingExperiment {
56+
static List<Object> values =
57+
Arrays.asList(
58+
"Hello world",
59+
46239847,
60+
-1984092375,
61+
Arrays.asList(1, "foo", 3, 5, 8, 10, 11),
62+
Arrays.asList(1, "foo", 9, 5, 8),
63+
Double.NaN,
64+
map("nested", "random"));
65+
66+
private Persistence persistence;
67+
private RemoteDocumentCache remoteDocumentCache;
68+
private MutationQueue mutationQueue;
69+
private DocumentOverlayCache documentOverlayCache;
70+
71+
protected IndexManager indexManager;
72+
protected QueryEngine queryEngine;
73+
74+
private @Nullable Boolean expectFullCollectionScan;
75+
76+
@Before
77+
public void setUp() {
78+
expectFullCollectionScan = null;
79+
80+
persistence = PersistenceTestHelpers.createSQLitePersistence();
81+
82+
indexManager = persistence.getIndexManager(User.UNAUTHENTICATED);
83+
mutationQueue = persistence.getMutationQueue(User.UNAUTHENTICATED, indexManager);
84+
documentOverlayCache = persistence.getDocumentOverlayCache(User.UNAUTHENTICATED);
85+
remoteDocumentCache = persistence.getRemoteDocumentCache();
86+
queryEngine = new QueryEngine();
87+
88+
indexManager.start();
89+
mutationQueue.start();
90+
91+
remoteDocumentCache.setIndexManager(indexManager);
92+
93+
LocalDocumentsView localDocuments =
94+
new LocalDocumentsView(
95+
remoteDocumentCache, mutationQueue, documentOverlayCache, indexManager) {
96+
@Override
97+
public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
98+
Query query, FieldIndex.IndexOffset offset) {
99+
assertEquals(
100+
"Observed query execution mode did not match expectation",
101+
expectFullCollectionScan,
102+
FieldIndex.IndexOffset.NONE.equals(offset));
103+
return super.getDocumentsMatchingQuery(query, offset);
104+
}
105+
};
106+
queryEngine.initialize(localDocuments, indexManager);
107+
}
108+
109+
/** Adds the provided documents to the remote document cache. */
110+
protected void addDocument(MutableDocument... docs) {
111+
persistence.runTransaction(
112+
"addDocument",
113+
() -> {
114+
for (MutableDocument doc : docs) {
115+
remoteDocumentCache.add(doc, doc.getVersion());
116+
}
117+
});
118+
}
119+
120+
protected void addMutation(Mutation mutation) {
121+
persistence.runTransaction(
122+
"addMutation",
123+
() -> {
124+
MutationBatch batch =
125+
mutationQueue.addMutationBatch(
126+
Timestamp.now(), Collections.emptyList(), Collections.singletonList(mutation));
127+
Map<DocumentKey, Mutation> overlayMap =
128+
Collections.singletonMap(mutation.getKey(), mutation);
129+
documentOverlayCache.saveOverlays(batch.getBatchId(), overlayMap);
130+
});
131+
}
132+
133+
protected <T> T expectOptimizedCollectionScan(Callable<T> c) throws Exception {
134+
try {
135+
expectFullCollectionScan = false;
136+
return c.call();
137+
} finally {
138+
expectFullCollectionScan = null;
139+
}
140+
}
141+
142+
private <T> T expectFullCollectionScan(Callable<T> c) throws Exception {
143+
try {
144+
expectFullCollectionScan = true;
145+
return c.call();
146+
} finally {
147+
expectFullCollectionScan = null;
148+
}
149+
}
150+
151+
protected DocumentSet runQuery(Query query, boolean usingIndex, QueryContext context) {
152+
Preconditions.checkNotNull(
153+
expectFullCollectionScan,
154+
"Encountered runQuery() call not wrapped in expectOptimizedCollectionQuery()/expectFullCollectionQuery()");
155+
ImmutableSortedMap<DocumentKey, Document> docs =
156+
queryEngine.getDocumentsMatchingQueryForTest(query, usingIndex, context);
157+
View view =
158+
new View(query, new ImmutableSortedSet<>(Collections.emptyList(), DocumentKey::compareTo));
159+
View.DocumentChanges viewDocChanges = view.computeDocChanges(docs);
160+
return view.applyChanges(viewDocChanges).getSnapshot().getDocuments();
161+
}
162+
163+
/** Creates one test document based on requirements. */
164+
private void createTestingDocument(
165+
String basePath, int documentID, boolean isMatched, int numOfFields) {
166+
Map<String, Object> fields = map("match", isMatched);
167+
168+
// Randomly generate the rest of fields.
169+
for (int i = 2; i <= numOfFields; i++) {
170+
// Randomly select a field in values table.
171+
int valueIndex = (int) (Math.random() * values.size()) % values.size();
172+
fields.put("field" + i, values.get(valueIndex));
173+
}
174+
175+
MutableDocument doc = doc(basePath + "/" + documentID, 1, fields);
176+
addDocument(doc);
177+
178+
indexManager.updateIndexEntries(docMap(doc));
179+
indexManager.updateCollectionGroup(basePath, FieldIndex.IndexOffset.fromDocument(doc));
180+
}
181+
182+
private void createTestingCollection(
183+
String basePath, int totalSetCount, int portion /*0 - 10*/, int numOfFields /* 1 - 30*/) {
184+
int documentCounter = 0;
185+
186+
// A set contains 10 documents.
187+
for (int i = 1; i <= totalSetCount; i++) {
188+
// Generate a random order list of 0 ... 9, to make sure the matching documents stay in
189+
// random positions.
190+
ArrayList<Integer> indexes = new ArrayList<>();
191+
for (int index = 0; index < 10; index++) {
192+
indexes.add(index);
193+
}
194+
Collections.shuffle(indexes);
195+
196+
// portion% of the set match
197+
for (int match = 0; match < portion; match++) {
198+
int currentID = documentCounter + indexes.get(match);
199+
createTestingDocument(basePath, currentID, true, numOfFields);
200+
}
201+
for (int unmatch = portion; unmatch < 10; unmatch++) {
202+
int currentID = documentCounter + indexes.get(unmatch);
203+
createTestingDocument(basePath, currentID, false, numOfFields);
204+
}
205+
documentCounter += 10;
206+
}
207+
}
208+
209+
/** Create mutation for 10% of total documents. */
210+
private void createMutationForCollection(String basePath, int totalSetCount) {
211+
ArrayList<Integer> indexes = new ArrayList<>();
212+
213+
// Randomly selects 10% of documents.
214+
for (int index = 0; index < totalSetCount * 10; index++) {
215+
indexes.add(index);
216+
}
217+
Collections.shuffle(indexes);
218+
219+
for (int i = 0; i < totalSetCount; i++) {
220+
addMutation(patchMutation(basePath + "/" + indexes.get(i), map("a", 5)));
221+
}
222+
}
223+
224+
@Test
225+
public void testCombinesIndexedWithNonIndexedResults() throws Exception {
226+
// Every set contains 10 documents
227+
final int numOfSet = 100;
228+
// could overflow. Currently it is safe when numOfSet set to 1000 and running on macbook M1
229+
long totalBeforeIndex = 0;
230+
long totalAfterIndex = 0;
231+
long totalDocumentCount = 0;
232+
long totalResultCount = 0;
233+
234+
// Temperate heuristic, gets when setting numOfSet to 1000.
235+
double without = 1;
236+
double with = 3;
237+
238+
for (int totalSetCount = 10; totalSetCount <= numOfSet; totalSetCount *= 10) {
239+
// portion stands for the percentage of documents matching query
240+
for (int portion = 0; portion <= 10; portion++) {
241+
for (int numOfFields = 1; numOfFields <= 31; numOfFields += 10) {
242+
String basePath = "documentCount" + totalSetCount;
243+
Query query = query(basePath).filter(filter("match", "==", true));
244+
245+
// Creates a full matched index for given query.
246+
indexManager.createTargetIndexes(query.toTarget());
247+
248+
createTestingCollection(basePath, totalSetCount, portion, numOfFields);
249+
createMutationForCollection(basePath, totalSetCount);
250+
251+
// runs query using full collection scan.
252+
QueryContext contextWithoutIndex = new QueryContext();
253+
long beforeAutoStart = System.nanoTime();
254+
DocumentSet results =
255+
expectFullCollectionScan(() -> runQuery(query, false, contextWithoutIndex));
256+
long beforeAutoEnd = System.nanoTime();
257+
long millisecondsBeforeAuto =
258+
TimeUnit.MILLISECONDS.convert(
259+
(beforeAutoEnd - beforeAutoStart), TimeUnit.NANOSECONDS);
260+
totalBeforeIndex += (beforeAutoEnd - beforeAutoStart);
261+
totalDocumentCount += contextWithoutIndex.getDocumentReadCount();
262+
assertEquals(portion * totalSetCount, results.size());
263+
264+
// runs query using index look up.
265+
QueryContext contextWithIndex = new QueryContext();
266+
long autoStart = System.nanoTime();
267+
results = expectOptimizedCollectionScan(() -> runQuery(query, true, contextWithIndex));
268+
long autoEnd = System.nanoTime();
269+
long millisecondsAfterAuto =
270+
TimeUnit.MILLISECONDS.convert((autoEnd - autoStart), TimeUnit.NANOSECONDS);
271+
totalAfterIndex += (autoEnd - autoStart);
272+
assertEquals(portion * totalSetCount, results.size());
273+
totalResultCount += results.size();
274+
275+
if (millisecondsBeforeAuto > millisecondsAfterAuto) {
276+
System.out.println(
277+
"Auto Indexing saves time when total of documents inside collection is "
278+
+ totalSetCount * 10
279+
+ ". The matching percentage is "
280+
+ portion
281+
+ "0%. And each document contains "
282+
+ numOfFields
283+
+ " fields.\n"
284+
+ "Weight result for without auto indexing is "
285+
+ without * contextWithoutIndex.getDocumentReadCount()
286+
+ ". And weight result for auto indexing is "
287+
+ with * results.size());
288+
}
289+
}
290+
}
291+
}
292+
293+
System.out.println(
294+
"The time heuristic is "
295+
+ (totalBeforeIndex / totalDocumentCount)
296+
+ " before auto indexing");
297+
System.out.println(
298+
"The time heuristic is " + (totalAfterIndex / totalResultCount) + " after auto indexing");
299+
}
300+
}

0 commit comments

Comments
 (0)