From 4ab3433cbe3429e1e29dfd3aa455313984604dc3 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 25 Aug 2023 15:25:52 -0400 Subject: [PATCH 1/8] persistent_cache_index_performance_experiment.ts skeleton added --- packages/firestore/src/api.ts | 2 + ...tent_cache_index_performance_experiment.ts | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 packages/firestore/src/api/persistent_cache_index_performance_experiment.ts diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index 9f9ac38749a..5585e4b934a 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -209,6 +209,8 @@ export { disablePersistentCacheIndexAutoCreation } from './api/persistent_cache_index_manager'; +export { runPersistentCacheIndexPerformanceExperiment } from './api/persistent_cache_index_performance_experiment'; + /** * Internal exports */ diff --git a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts new file mode 100644 index 00000000000..bdae4e91433 --- /dev/null +++ b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { User } from '../auth/user'; +import { DatabaseId } from '../core/database_info'; +import { IndexedDbIndexManager } from '../local/indexeddb_index_manager'; +import { IndexedDbPersistence } from '../local/indexeddb_persistence'; +import { LocalDocumentsView } from '../local/local_documents_view'; +import { LruParams } from '../local/lru_garbage_collector'; +import { QueryEngine } from '../local/query_engine'; +import { getDocument, getWindow } from '../platform/dom'; +import { JsonProtoSerializer } from '../remote/serializer'; +import { AsyncQueueImpl } from '../util/async_queue_impl'; +import { AutoId } from '../util/misc'; + +export function runPersistentCacheIndexPerformanceExperiment( + log: (...args: unknown[]) => unknown +): void { + const { queryEngine } = createTestObjects(); + log('Created QueryEngine', queryEngine); +} + +interface TestObjects { + queryEngine: QueryEngine; +} + +function createTestObjects(): TestObjects { + const databaseId = new DatabaseId(/*projectId=*/ AutoId.newId()); + const user = new User(/*uid=*/ null); + const persistence = new IndexedDbPersistence( + /*allowTabSynchronization=*/ false, + /*persistenceKey=*/ AutoId.newId(), + /*clientId=*/ AutoId.newId(), + /*lruParams=*/ LruParams.DISABLED, + /*queue=*/ new AsyncQueueImpl(), + /*window=*/ getWindow(), + /*document=*/ getDocument(), + /*serializer=*/ new JsonProtoSerializer( + databaseId, + /*useProto3Json=*/ true + ), + /*sequenceNumberSyncer=*/ { + writeSequenceNumber(_: unknown): void {}, + sequenceNumberHandler: null + }, + /*forceOwningTab=*/ false + ); + + const remoteDocumentCache = persistence.getRemoteDocumentCache(); + const indexManager = new IndexedDbIndexManager(user, databaseId); + const mutationQueue = persistence.getMutationQueue(user, indexManager); + const documentOverlayCache = persistence.getDocumentOverlayCache(user); + const localDocumentView = new LocalDocumentsView( + remoteDocumentCache, + mutationQueue, + documentOverlayCache, + indexManager + ); + const queryEngine = new QueryEngine(); + queryEngine.initialize(localDocumentView, indexManager); + + return { queryEngine }; +} From 82de1e146747f81a00009e14ef8309f0dd72645b Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 28 Aug 2023 16:29:52 -0400 Subject: [PATCH 2/8] more work --- ...tent_cache_index_performance_experiment.ts | 141 +++++++++++++++++- .../test/integration/api/temp.test.ts | 39 +++++ 2 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 packages/firestore/test/integration/api/temp.test.ts diff --git a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts index bdae4e91433..c8181c1e68e 100644 --- a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts +++ b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts @@ -17,28 +17,139 @@ import { User } from '../auth/user'; import { DatabaseId } from '../core/database_info'; +import { FieldFilter, Operator } from '../core/filter'; +import { QueryImpl, queryToTarget } from '../core/query'; +import { SnapshotVersion } from '../core/snapshot_version'; +import { IndexManager } from '../local/index_manager'; import { IndexedDbIndexManager } from '../local/indexeddb_index_manager'; import { IndexedDbPersistence } from '../local/indexeddb_persistence'; import { LocalDocumentsView } from '../local/local_documents_view'; import { LruParams } from '../local/lru_garbage_collector'; +import { Persistence } from '../local/persistence'; +import { PersistencePromise } from '../local/persistence_promise'; import { QueryEngine } from '../local/query_engine'; +import { documentKeySet, documentMap } from '../model/collections'; +import { DocumentKey } from '../model/document_key'; +import { IndexOffset } from '../model/field_index'; +import { ObjectValue } from '../model/object_value'; +import { FieldPath, ResourcePath } from '../model/path'; import { getDocument, getWindow } from '../platform/dom'; import { JsonProtoSerializer } from '../remote/serializer'; import { AsyncQueueImpl } from '../util/async_queue_impl'; import { AutoId } from '../util/misc'; -export function runPersistentCacheIndexPerformanceExperiment( +import { Timestamp } from './timestamp'; + + +interface ExperimentConfig { + /** The number of documents to create in the collection. */ + documentCount: number; + /** The number of fields in each document. */ + fieldCount: number; + /** The number of documents that match the query. */ + documentMatchCount: number; +} + +export async function runPersistentCacheIndexPerformanceExperiment( + config: ExperimentConfig, log: (...args: unknown[]) => unknown -): void { - const { queryEngine } = createTestObjects(); - log('Created QueryEngine', queryEngine); +): Promise { + const { persistence, indexManager, queryEngine } = await createTestObjects(); + const collectionId = AutoId.newId(); + + const query = createQuery(collectionId, 'matches', Operator.EQUAL, true); + const target = queryToTarget(query); + await persistence.runTransaction('createTargetIndexes', 'readwrite', txn => { + log('createTargetIndexes()'); + return indexManager.createTargetIndexes(txn, queryToTarget(query)); + }); + + await persistence.runTransaction('populate collection', 'readwrite', txn => { + log('populate collection'); + const documentIds: string[] = []; + for (let i = 0; i < config.documentCount; i++) { + documentIds.push(AutoId.newId()); + } + const matchingDocumentIds = new Set(); + while (matchingDocumentIds.size < config.documentMatchCount) { + const matchingDocumentIdIndex = Math.floor( + Math.random() * documentIds.length + ); + matchingDocumentIds.add(documentIds[matchingDocumentIdIndex]); + } + const documents: Array<{ documentId: string; value: ObjectValue }> = []; + for (const documentId of documentIds) { + const value = ObjectValue.empty(); + for (let fieldIndex = 0; fieldIndex < config.fieldCount; fieldIndex++) { + const fieldPath = new FieldPath([AutoId.newId()]); + value.set(fieldPath, { stringValue: `field${fieldIndex}` }); + } + if (matchingDocumentIds.has(documentId)) { + value.set(new FieldPath(['matches']), { booleanValue: true }); + } + documents.push({ documentId, value }); + } + return PersistencePromise.forEach( + documents, + (documentInfo: { documentId: string; value: ObjectValue }) => { + const { documentId, value } = documentInfo; + const documentKey = DocumentKey.fromSegments([ + collectionId, + documentId + ]); + const changeBuffer = persistence + .getRemoteDocumentCache() + .newChangeBuffer(); + return changeBuffer.getEntry(txn, documentKey).next(document => { + changeBuffer.addEntry( + document.convertToFoundDocument( + SnapshotVersion.fromTimestamp(Timestamp.fromMillis(1)), + value + ) + ); + return changeBuffer + .apply(txn) + .next(() => + indexManager.updateIndexEntries(txn, documentMap(document)) + ) + .next(() => + indexManager.updateCollectionGroup( + txn, + collectionId, + new IndexOffset(document.readTime, document.key, -1) + ) + ); + }); + } + ); + }); + + const queryResult = await persistence.runTransaction( + 'populate collection', + 'readwrite', + txn => { + log('getDocumentsMatchingQuery()'); + return queryEngine.getDocumentsMatchingQuery( + txn, + query, + SnapshotVersion.min(), + documentKeySet() + ); + } + ); + + log(`getDocumentsMatchingQuery() returned ${queryResult.size} documents`); + + await persistence.shutdown(); } interface TestObjects { + persistence: Persistence; + indexManager: IndexManager; queryEngine: QueryEngine; } -function createTestObjects(): TestObjects { +async function createTestObjects(): Promise { const databaseId = new DatabaseId(/*projectId=*/ AutoId.newId()); const user = new User(/*uid=*/ null); const persistence = new IndexedDbPersistence( @@ -60,6 +171,8 @@ function createTestObjects(): TestObjects { /*forceOwningTab=*/ false ); + await persistence.start(); + const remoteDocumentCache = persistence.getRemoteDocumentCache(); const indexManager = new IndexedDbIndexManager(user, databaseId); const mutationQueue = persistence.getMutationQueue(user, indexManager); @@ -73,5 +186,21 @@ function createTestObjects(): TestObjects { const queryEngine = new QueryEngine(); queryEngine.initialize(localDocumentView, indexManager); - return { queryEngine }; + return { persistence, indexManager, queryEngine }; +} + +function createQuery( + path: string, + field: string, + op: Operator, + value: boolean +): QueryImpl { + const fieldPath = FieldPath.fromServerFormat(field); + const filter = FieldFilter.create(fieldPath, op, { booleanValue: value }); + return new QueryImpl( + /*path=*/ ResourcePath.fromString(path), + /*collectionGroup=*/ null, + /*explicitOrderBy=*/ [], + /*filters=*/ [filter] + ); } diff --git a/packages/firestore/test/integration/api/temp.test.ts b/packages/firestore/test/integration/api/temp.test.ts new file mode 100644 index 00000000000..c9add1b00aa --- /dev/null +++ b/packages/firestore/test/integration/api/temp.test.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { runPersistentCacheIndexPerformanceExperiment } from '../util/firebase_export'; +import { + apiDescribe +} from '../util/helpers'; + +apiDescribe('experiment', persistence => { + it.only('run experiment', function () { + if (persistence.storage === 'indexeddb') { + return runPersistentCacheIndexPerformanceExperiment( + { + documentCount: 100, + documentMatchCount: 5, + fieldCount: 31 + }, + console.log + ); + } else { + this.skip(); + } + }); +}); From 8d9f4a2e0ac1371a6ca6ec0fbc49e6dac63c6a9f Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 29 Aug 2023 14:20:41 -0400 Subject: [PATCH 3/8] add mutation --- ...tent_cache_index_performance_experiment.ts | 494 ++++++++++++++---- .../test/integration/api/temp.test.ts | 16 +- 2 files changed, 394 insertions(+), 116 deletions(-) diff --git a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts index c8181c1e68e..7c6ac8ccd7e 100644 --- a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts +++ b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts @@ -20,138 +20,277 @@ import { DatabaseId } from '../core/database_info'; import { FieldFilter, Operator } from '../core/filter'; import { QueryImpl, queryToTarget } from '../core/query'; import { SnapshotVersion } from '../core/snapshot_version'; +import { parseUpdateData, UserDataReader } from '../lite-api/user_data_reader'; +import { DocumentOverlayCache } from '../local/document_overlay_cache'; import { IndexManager } from '../local/index_manager'; import { IndexedDbIndexManager } from '../local/indexeddb_index_manager'; import { IndexedDbPersistence } from '../local/indexeddb_persistence'; import { LocalDocumentsView } from '../local/local_documents_view'; import { LruParams } from '../local/lru_garbage_collector'; +import { MutationQueue } from '../local/mutation_queue'; import { Persistence } from '../local/persistence'; -import { PersistencePromise } from '../local/persistence_promise'; import { QueryEngine } from '../local/query_engine'; -import { documentKeySet, documentMap } from '../model/collections'; +import { RemoteDocumentCache } from '../local/remote_document_cache'; +import { documentMap, newMutationMap } from '../model/collections'; +import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { IndexOffset } from '../model/field_index'; -import { ObjectValue } from '../model/object_value'; +import { IndexOffset, INITIAL_LARGEST_BATCH_ID } from '../model/field_index'; +import { FieldMask } from '../model/field_mask'; +import { + FieldTransform, + Mutation, + PatchMutation, + Precondition +} from '../model/mutation'; +import { JsonObject, ObjectValue } from '../model/object_value'; import { FieldPath, ResourcePath } from '../model/path'; import { getDocument, getWindow } from '../platform/dom'; +import { Value as ProtoValue } from '../protos/firestore_proto_api'; import { JsonProtoSerializer } from '../remote/serializer'; import { AsyncQueueImpl } from '../util/async_queue_impl'; import { AutoId } from '../util/misc'; +import { SortedSet } from '../util/sorted_set'; import { Timestamp } from './timestamp'; - -interface ExperimentConfig { - /** The number of documents to create in the collection. */ - documentCount: number; - /** The number of fields in each document. */ - fieldCount: number; - /** The number of documents that match the query. */ - documentMatchCount: number; -} - export async function runPersistentCacheIndexPerformanceExperiment( - config: ExperimentConfig, log: (...args: unknown[]) => unknown ): Promise { - const { persistence, indexManager, queryEngine } = await createTestObjects(); - const collectionId = AutoId.newId(); - - const query = createQuery(collectionId, 'matches', Operator.EQUAL, true); - const target = queryToTarget(query); - await persistence.runTransaction('createTargetIndexes', 'readwrite', txn => { - log('createTargetIndexes()'); - return indexManager.createTargetIndexes(txn, queryToTarget(query)); - }); + const testObjects = await createTestObjects(); + const experiment = new AutoIndexingExperiment(log, testObjects); + await experiment.run(); + await testObjects.persistence.shutdown(); +} - await persistence.runTransaction('populate collection', 'readwrite', txn => { - log('populate collection'); - const documentIds: string[] = []; - for (let i = 0; i < config.documentCount; i++) { - documentIds.push(AutoId.newId()); - } - const matchingDocumentIds = new Set(); - while (matchingDocumentIds.size < config.documentMatchCount) { - const matchingDocumentIdIndex = Math.floor( - Math.random() * documentIds.length - ); - matchingDocumentIds.add(documentIds[matchingDocumentIdIndex]); +interface TestObjects { + persistence: Persistence; + indexManager: IndexManager; + remoteDocumentCache: RemoteDocumentCache; + queryEngine: QueryEngine; + serializer: JsonProtoSerializer; + mutationQueue: MutationQueue; + documentOverlayCache: DocumentOverlayCache; +} + +class AutoIndexingExperiment { + private readonly logFunc: (...args: unknown[]) => unknown; + private readonly persistence: Persistence; + private readonly indexManager: IndexManager; + private readonly remoteDocumentCache: RemoteDocumentCache; + private readonly queryEngine: QueryEngine; + private readonly serializer: JsonProtoSerializer; + private readonly mutationQueue: MutationQueue; + private readonly documentOverlayCache: DocumentOverlayCache; + + constructor( + logFunc: (...args: unknown[]) => unknown, + testObjects: TestObjects + ) { + this.logFunc = logFunc; + this.persistence = testObjects.persistence; + this.indexManager = testObjects.indexManager; + this.remoteDocumentCache = testObjects.remoteDocumentCache; + this.queryEngine = testObjects.queryEngine; + this.serializer = testObjects.serializer; + this.mutationQueue = testObjects.mutationQueue; + this.documentOverlayCache = testObjects.documentOverlayCache; + } + + async run(): Promise { + // Every set contains 10 documents + const numOfSet = 100; + // could overflow. Currently it is safe when numOfSet set to 1000 and running on macbook M1 + const totalBeforeIndex = 0; + const totalAfterIndex = 0; + const totalDocumentCount = 0; + const totalResultCount = 0; + + // Temperate heuristic, gets when setting numOfSet to 1000. + const withoutIndex = 1; + const withIndex = 3; + + for ( + let totalSetCount = 10; + totalSetCount <= numOfSet; + totalSetCount *= 10 + ) { + // portion stands for the percentage of documents matching query + for (let portion = 0; portion <= 10; portion++) { + for (let numOfFields = 1; numOfFields <= 31; numOfFields += 10) { + const basePath = 'documentCount' + totalSetCount; + const query = createQuery(basePath, 'match', Operator.EQUAL, true); + + // Creates a full matched index for given query. + await this.persistence.runTransaction( + 'createTargetIndexes', + 'readwrite', + txn => + this.indexManager.createTargetIndexes(txn, queryToTarget(query)) + ); + + await this.createTestingCollection( + basePath, + totalSetCount, + portion, + numOfFields + ); + await this.createMutationForCollection(basePath, totalSetCount); + } + } } - const documents: Array<{ documentId: string; value: ObjectValue }> = []; - for (const documentId of documentIds) { - const value = ObjectValue.empty(); - for (let fieldIndex = 0; fieldIndex < config.fieldCount; fieldIndex++) { - const fieldPath = new FieldPath([AutoId.newId()]); - value.set(fieldPath, { stringValue: `field${fieldIndex}` }); + } + + async createTestingCollection( + basePath: string, + totalSetCount: number, + portion: number /*0 - 10*/, + numOfFields: number /* 1 - 30*/ + ): Promise { + this.log( + `Creating test collection: "${basePath}" ` + + `totalSetCount=${totalSetCount} ` + + `portion=${portion} ` + + `numOfFields=${numOfFields}` + ); + let documentCounter = 0; + + // A set contains 10 documents. + for (let i = 1; i <= totalSetCount; i++) { + // Generate a random order list of 0 ... 9, to make sure the matching + // documents stay in random positions. + const indexes: number[] = []; + for (let index = 0; index < 10; index++) { + indexes.push(index); + } + shuffle(indexes); + + // portion% of the set match + for (let match = 0; match < portion; match++) { + const currentID = documentCounter + indexes[match]; + await this.createTestingDocument( + basePath, + currentID, + true, + numOfFields + ); } - if (matchingDocumentIds.has(documentId)) { - value.set(new FieldPath(['matches']), { booleanValue: true }); + for (let unmatch = portion; unmatch < 10; unmatch++) { + const currentID = documentCounter + indexes[unmatch]; + await this.createTestingDocument( + basePath, + currentID, + false, + numOfFields + ); } - documents.push({ documentId, value }); + documentCounter += 10; } - return PersistencePromise.forEach( - documents, - (documentInfo: { documentId: string; value: ObjectValue }) => { - const { documentId, value } = documentInfo; - const documentKey = DocumentKey.fromSegments([ - collectionId, - documentId - ]); - const changeBuffer = persistence - .getRemoteDocumentCache() - .newChangeBuffer(); - return changeBuffer.getEntry(txn, documentKey).next(document => { - changeBuffer.addEntry( - document.convertToFoundDocument( - SnapshotVersion.fromTimestamp(Timestamp.fromMillis(1)), - value - ) - ); - return changeBuffer - .apply(txn) - .next(() => - indexManager.updateIndexEntries(txn, documentMap(document)) - ) - .next(() => - indexManager.updateCollectionGroup( - txn, - collectionId, - new IndexOffset(document.readTime, document.key, -1) - ) - ); - }); - } - ); - }); + } - const queryResult = await persistence.runTransaction( - 'populate collection', - 'readwrite', - txn => { - log('getDocumentsMatchingQuery()'); - return queryEngine.getDocumentsMatchingQuery( - txn, - query, - SnapshotVersion.min(), - documentKeySet() + async createMutationForCollection( + basePath: string, + totalSetCount: number + ): Promise { + const indexes: number[] = []; + // Randomly selects 10% of documents. + for (let index = 0; index < totalSetCount * 10; index++) { + indexes.push(index); + } + shuffle(indexes); + + for (let i = 0; i < totalSetCount; i++) { + await this.addMutation( + createPatchMutation( + `${basePath}/${indexes[i]}`, + { a: 5 }, + /*precondition=*/ null, + this.serializer + ) ); } - ); + } - log(`getDocumentsMatchingQuery() returned ${queryResult.size} documents`); + /** Creates one test document based on requirements. */ + async createTestingDocument( + basePath: string, + documentID: number, + isMatched: boolean, + numOfFields: number + ): Promise { + const fields = new Map(); + fields.set('match', isMatched); - await persistence.shutdown(); -} + // Randomly generate the rest of fields. + for (let i = 2; i <= numOfFields; i++) { + // Randomly select a field in values table. + const valueIndex = Math.floor(Math.random() * values.length); + fields.set('field' + i, values[valueIndex]); + } -interface TestObjects { - persistence: Persistence; - indexManager: IndexManager; - queryEngine: QueryEngine; + const doc = createMutableDocument(basePath + '/' + documentID, 1, fields); + await this.addDocument(doc); + + await this.persistence.runTransaction( + 'updateIndexEntries', + 'readwrite', + txn => this.indexManager.updateIndexEntries(txn, documentMap(doc)) + ); + await this.persistence.runTransaction( + 'updateCollectionGroup', + 'readwrite', + txn => + this.indexManager.updateCollectionGroup( + txn, + basePath, + new IndexOffset(doc.readTime, doc.key, INITIAL_LARGEST_BATCH_ID) + ) + ); + } + + /** Adds the provided documents to the remote document cache. */ + addDocument(doc: MutableDocument): Promise { + return this.persistence.runTransaction('addDocument', 'readwrite', txn => { + const changeBuffer = this.remoteDocumentCache.newChangeBuffer(); + return changeBuffer + .getEntry(txn, doc.key) + .next(() => changeBuffer.addEntry(doc)) + .next(() => changeBuffer.apply(txn)); + }); + } + + addMutation(mutation: Mutation): Promise { + return this.persistence.runTransaction('addMutation', 'readwrite', txn => + this.mutationQueue + .addMutationBatch( + txn, + /*localWriteTime=*/ Timestamp.now(), + /*baseMutations=*/ [], + /*mutations=*/ [mutation] + ) + .next(batch => { + const overlayMap = newMutationMap(); + overlayMap.set(mutation.key, mutation); + return this.documentOverlayCache.saveOverlays( + txn, + batch.batchId, + overlayMap + ); + }) + ); + } + + log(...args: unknown[]): void { + this.logFunc(...args); + } } async function createTestObjects(): Promise { const databaseId = new DatabaseId(/*projectId=*/ AutoId.newId()); const user = new User(/*uid=*/ null); + const serializer = new JsonProtoSerializer( + databaseId, + /*useProto3Json=*/ true + ); const persistence = new IndexedDbPersistence( /*allowTabSynchronization=*/ false, /*persistenceKey=*/ AutoId.newId(), @@ -160,10 +299,7 @@ async function createTestObjects(): Promise { /*queue=*/ new AsyncQueueImpl(), /*window=*/ getWindow(), /*document=*/ getDocument(), - /*serializer=*/ new JsonProtoSerializer( - databaseId, - /*useProto3Json=*/ true - ), + /*serializer=*/ serializer, /*sequenceNumberSyncer=*/ { writeSequenceNumber(_: unknown): void {}, sequenceNumberHandler: null @@ -173,8 +309,9 @@ async function createTestObjects(): Promise { await persistence.start(); - const remoteDocumentCache = persistence.getRemoteDocumentCache(); const indexManager = new IndexedDbIndexManager(user, databaseId); + const remoteDocumentCache = persistence.getRemoteDocumentCache(); + remoteDocumentCache.setIndexManager(indexManager); const mutationQueue = persistence.getMutationQueue(user, indexManager); const documentOverlayCache = persistence.getDocumentOverlayCache(user); const localDocumentView = new LocalDocumentsView( @@ -186,7 +323,15 @@ async function createTestObjects(): Promise { const queryEngine = new QueryEngine(); queryEngine.initialize(localDocumentView, indexManager); - return { persistence, indexManager, queryEngine }; + return { + persistence, + indexManager, + remoteDocumentCache, + queryEngine, + serializer, + mutationQueue, + documentOverlayCache + }; } function createQuery( @@ -204,3 +349,146 @@ function createQuery( /*filters=*/ [filter] ); } + +function createMutableDocument( + key: string, + version: number, + data: Map +): MutableDocument { + const documentKey = DocumentKey.fromPath(key); + const snapshotVersion = SnapshotVersion.fromTimestamp( + Timestamp.fromMillis(version) + ); + const documentData = wrapObject(data); + return MutableDocument.newFoundDocument( + documentKey, + snapshotVersion, + snapshotVersion, + documentData + ).setReadTime(snapshotVersion); +} + +function wrapObject(value: Map): ObjectValue { + const result = ObjectValue.empty(); + value.forEach((fieldValue, fieldName) => { + const fieldPath = FieldPath.fromServerFormat(fieldName); + const fieldProtoValue = createProtoValue(fieldValue); + result.set(fieldPath, fieldProtoValue); + }); + return result; +} + +function createProtoValue(value: unknown): ProtoValue { + if (value === null) { + return { nullValue: 'NULL_VALUE' }; + } + + if (typeof value === 'number') { + return Number.isInteger(value) + ? { integerValue: value } + : { doubleValue: value }; + } + + if (typeof value === 'string') { + return { stringValue: value }; + } + + if (typeof value === 'boolean') { + return { booleanValue: value }; + } + + if (typeof value !== 'object') { + throw new Error(`unsupported object type: ${typeof value}`); + } + + if (Array.isArray(value)) { + return { arrayValue: { values: value.map(createProtoValue) } }; + } + + const fields: Record = {}; + for (const [fieldName, fieldValue] of Object.entries(value)) { + fields[fieldName] = createProtoValue(fieldValue); + } + return { mapValue: { fields } }; +} + +function createPatchMutation( + keyStr: string, + json: JsonObject, + precondition: Precondition | null, + serializer: JsonProtoSerializer +): PatchMutation { + if (precondition === null) { + precondition = Precondition.exists(true); + } + return patchMutationHelper( + keyStr, + json, + precondition, + /* updateMask */ null, + serializer + ); +} + +function patchMutationHelper( + keyStr: string, + json: JsonObject, + precondition: Precondition, + updateMask: FieldPath[] | null, + serializer: JsonProtoSerializer +): PatchMutation { + const patchKey = DocumentKey.fromPath(keyStr); + const parsed = parseUpdateData( + new UserDataReader(serializer.databaseId, false, serializer), + 'patchMutation', + patchKey, + json + ); + + // `mergeMutation()` provides an update mask for the merged fields, whereas + // `patchMutation()` requires the update mask to be parsed from the values. + const mask = updateMask ? updateMask : parsed.fieldMask.fields; + + // We sort the fieldMaskPaths to make the order deterministic in tests. + // (Otherwise, when we flatten a Set to a proto repeated field, we'll end up + // comparing in iterator order and possibly consider {foo,bar} != {bar,foo}.) + let fieldMaskPaths = new SortedSet(FieldPath.comparator); + mask.forEach(value => (fieldMaskPaths = fieldMaskPaths.add(value))); + + // The order of the transforms doesn't matter, but we sort them so tests can + // assume a particular order. + const fieldTransforms: FieldTransform[] = []; + fieldTransforms.push(...parsed.fieldTransforms); + fieldTransforms.sort((lhs, rhs) => + FieldPath.comparator(lhs.field, rhs.field) + ); + + return new PatchMutation( + patchKey, + parsed.data, + new FieldMask(fieldMaskPaths.toArray()), + precondition, + fieldTransforms + ); +} + +function shuffle(array: T[]): void { + const shuffled = array + .map(element => { + return { element, randomValue: Math.random() }; + }) + .sort((e1, e2) => e1.randomValue - e2.randomValue); + for (let i = 0; i < array.length; i++) { + array[i] = shuffled[i].element; + } +} + +const values = Object.freeze([ + 'Hello world', + 46239847, + -1984092375, + Object.freeze([1, 'foo', 3, 5, 8, 10, 11]), + Object.freeze([1, 'foo', 9, 5, 8]), + Number.NaN, + Object.freeze({ 'nested': 'random' }) +]); diff --git a/packages/firestore/test/integration/api/temp.test.ts b/packages/firestore/test/integration/api/temp.test.ts index c9add1b00aa..7fb7a24eef1 100644 --- a/packages/firestore/test/integration/api/temp.test.ts +++ b/packages/firestore/test/integration/api/temp.test.ts @@ -15,25 +15,15 @@ * limitations under the License. */ - import { runPersistentCacheIndexPerformanceExperiment } from '../util/firebase_export'; -import { - apiDescribe -} from '../util/helpers'; +import { apiDescribe } from '../util/helpers'; apiDescribe('experiment', persistence => { it.only('run experiment', function () { if (persistence.storage === 'indexeddb') { - return runPersistentCacheIndexPerformanceExperiment( - { - documentCount: 100, - documentMatchCount: 5, - fieldCount: 31 - }, - console.log - ); + return runPersistentCacheIndexPerformanceExperiment(console.log); } else { this.skip(); } - }); + }).timeout(60000); }); From 43ff0b1d697cd98604fa83f07e21236f447b1e92 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 29 Aug 2023 15:06:58 -0400 Subject: [PATCH 4/8] experiment completed --- ...tent_cache_index_performance_experiment.ts | 107 ++++++++++++++++-- packages/firestore/src/local/query_engine.ts | 4 +- .../test/integration/api/temp.test.ts | 2 +- 3 files changed, 100 insertions(+), 13 deletions(-) diff --git a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts index 7c6ac8ccd7e..9a40591412e 100644 --- a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts +++ b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts @@ -52,12 +52,14 @@ import { AutoId } from '../util/misc'; import { SortedSet } from '../util/sorted_set'; import { Timestamp } from './timestamp'; +import { QueryContext } from '../local/query_context'; export async function runPersistentCacheIndexPerformanceExperiment( - log: (...args: unknown[]) => unknown + log: (...args: unknown[]) => unknown, + logLevel: 'info' | 'debug' ): Promise { const testObjects = await createTestObjects(); - const experiment = new AutoIndexingExperiment(log, testObjects); + const experiment = new AutoIndexingExperiment(log, logLevel, testObjects); await experiment.run(); await testObjects.persistence.shutdown(); } @@ -73,7 +75,6 @@ interface TestObjects { } class AutoIndexingExperiment { - private readonly logFunc: (...args: unknown[]) => unknown; private readonly persistence: Persistence; private readonly indexManager: IndexManager; private readonly remoteDocumentCache: RemoteDocumentCache; @@ -83,7 +84,8 @@ class AutoIndexingExperiment { private readonly documentOverlayCache: DocumentOverlayCache; constructor( - logFunc: (...args: unknown[]) => unknown, + private readonly logFunc: (...args: unknown[]) => unknown, + private readonly logLevel: 'info' | 'debug', testObjects: TestObjects ) { this.logFunc = logFunc; @@ -100,10 +102,10 @@ class AutoIndexingExperiment { // Every set contains 10 documents const numOfSet = 100; // could overflow. Currently it is safe when numOfSet set to 1000 and running on macbook M1 - const totalBeforeIndex = 0; - const totalAfterIndex = 0; - const totalDocumentCount = 0; - const totalResultCount = 0; + let totalBeforeIndex = 0; + let totalAfterIndex = 0; + let totalDocumentCount = 0; + let totalResultCount = 0; // Temperate heuristic, gets when setting numOfSet to 1000. const withoutIndex = 1; @@ -117,7 +119,9 @@ class AutoIndexingExperiment { // portion stands for the percentage of documents matching query for (let portion = 0; portion <= 10; portion++) { for (let numOfFields = 1; numOfFields <= 31; numOfFields += 10) { - const basePath = 'documentCount' + totalSetCount; + const basePath = + `${AutoId.newId()}_totalSetCount${totalSetCount}_` + + `portion${portion}_numOfFields${numOfFields}`; const query = createQuery(basePath, 'match', Operator.EQUAL, true); // Creates a full matched index for given query. @@ -135,6 +139,83 @@ class AutoIndexingExperiment { numOfFields ); await this.createMutationForCollection(basePath, totalSetCount); + + // runs query using full collection scan. + let millisecondsBeforeAuto: number; + let contextWithoutIndexDocumentReadCount: number; + { + const contextWithoutIndex = new QueryContext(); + const beforeAutoStart = performance.now(); + const beforeAutoResults = await this.persistence.runTransaction( + 'executeFullCollectionScan', + 'readwrite', + txn => + this.queryEngine.executeFullCollectionScan( + txn, + query, + contextWithoutIndex + ) + ); + const beforeAutoEnd = performance.now(); + millisecondsBeforeAuto = beforeAutoEnd - beforeAutoStart; + totalBeforeIndex += millisecondsBeforeAuto; + totalDocumentCount += contextWithoutIndex.documentReadCount; + contextWithoutIndexDocumentReadCount = + contextWithoutIndex.documentReadCount; + if (portion * totalSetCount != beforeAutoResults.size) { + throw new Error( + `${ + portion * totalSetCount + }!={beforeAutoResults.size} (portion * totalSetCount != beforeAutoResults.size)` + ); + } + this.logDebug( + `Running query without using the index took ${millisecondsBeforeAuto}ms` + ); + } + + // runs query using index look up. + let millisecondsAfterAuto: number; + let autoResultsSize: number; + { + const autoStart = performance.now(); + const autoResults = await this.persistence.runTransaction( + 'performQueryUsingIndex', + 'readwrite', + txn => this.queryEngine.performQueryUsingIndex(txn, query) + ); + if (autoResults === null) { + throw new Error('performQueryUsingIndex() returned null'); + } + const autoEnd = performance.now(); + millisecondsAfterAuto = autoEnd - autoStart; + totalAfterIndex += millisecondsAfterAuto; + if (portion * totalSetCount != autoResults.size) { + throw new Error( + `${ + portion * totalSetCount + }!={beforeAutoResults.size} (portion * totalSetCount != beforeAutoResults.size)` + ); + } + this.logDebug( + `Running query using the index took ${millisecondsAfterAuto}ms` + ); + totalResultCount += autoResults.size; + autoResultsSize = autoResults.size; + } + + if (millisecondsBeforeAuto > millisecondsAfterAuto) { + this.log( + `Auto Indexing saves time when total of documents inside ` + + `collection is ${totalSetCount * 10}. ` + + `The matching percentage is ${portion}0%. ` + + `And each document contains ${numOfFields} fields. ` + + `Weight result for without auto indexing is ` + + `${withoutIndex * contextWithoutIndexDocumentReadCount}. ` + + `And weight result for auto indexing is ` + + `${withIndex * autoResultsSize}` + ); + } } } } @@ -146,7 +227,7 @@ class AutoIndexingExperiment { portion: number /*0 - 10*/, numOfFields: number /* 1 - 30*/ ): Promise { - this.log( + this.logDebug( `Creating test collection: "${basePath}" ` + `totalSetCount=${totalSetCount} ` + `portion=${portion} ` + @@ -279,6 +360,12 @@ class AutoIndexingExperiment { ); } + logDebug(...args: unknown[]): void { + if (this.logLevel === 'debug') { + this.logFunc(...args); + } + } + log(...args: unknown[]): void { this.logFunc(...args); } diff --git a/packages/firestore/src/local/query_engine.ts b/packages/firestore/src/local/query_engine.ts index a0adb7ed95a..dd4b3cee729 100644 --- a/packages/firestore/src/local/query_engine.ts +++ b/packages/firestore/src/local/query_engine.ts @@ -236,7 +236,7 @@ export class QueryEngine { * Performs an indexed query that evaluates the query based on a collection's * persisted index values. Returns `null` if an index is not available. */ - private performQueryUsingIndex( + performQueryUsingIndex( transaction: PersistenceTransaction, query: Query ): PersistencePromise { @@ -448,7 +448,7 @@ export class QueryEngine { ); } - private executeFullCollectionScan( + public executeFullCollectionScan( transaction: PersistenceTransaction, query: Query, context: QueryContext diff --git a/packages/firestore/test/integration/api/temp.test.ts b/packages/firestore/test/integration/api/temp.test.ts index 7fb7a24eef1..9d3544facba 100644 --- a/packages/firestore/test/integration/api/temp.test.ts +++ b/packages/firestore/test/integration/api/temp.test.ts @@ -21,7 +21,7 @@ import { apiDescribe } from '../util/helpers'; apiDescribe('experiment', persistence => { it.only('run experiment', function () { if (persistence.storage === 'indexeddb') { - return runPersistentCacheIndexPerformanceExperiment(console.log); + return runPersistentCacheIndexPerformanceExperiment(console.log, 'info'); } else { this.skip(); } From bf3c6f18a62d6bcf2503163b3b275af6e5d83aa9 Mon Sep 17 00:00:00 2001 From: dconeybe Date: Fri, 1 Sep 2023 05:07:46 +0000 Subject: [PATCH 5/8] Update API reports --- common/api-review/firestore.api.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index ada53f25a59..4fcb0cf9e71 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -584,6 +584,9 @@ export class QueryStartAtConstraint extends QueryConstraint { // @public export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; +// @public +export function runPersistentCacheIndexPerformanceExperiment(log: (...args: unknown[]) => unknown, logLevel: 'info' | 'debug'): Promise; + // @public export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise, options?: TransactionOptions): Promise; From 445e6776b0079192f85a2719d7b734d78e3d9798 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 1 Sep 2023 10:26:22 -0400 Subject: [PATCH 6/8] add final log output that I forgot --- .../api/persistent_cache_index_performance_experiment.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts index 9a40591412e..f7143763fe7 100644 --- a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts +++ b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts @@ -219,6 +219,15 @@ class AutoIndexingExperiment { } } } + + this.log( + `The time heuristic is ` + + `${totalBeforeIndex / totalDocumentCount} before auto indexing` + ); + this.log( + `The time heuristic is ` + + `${totalAfterIndex / totalResultCount} after auto indexing` + ); } async createTestingCollection( From de3df0db3d1871f857f35acfddc27ef45b7d03b8 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 5 Sep 2023 14:15:13 -0400 Subject: [PATCH 7/8] persistent_cache_index_performance_experiment.ts: return the heuristic --- ...persistent_cache_index_performance_experiment.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts index f7143763fe7..891deba2969 100644 --- a/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts +++ b/packages/firestore/src/api/persistent_cache_index_performance_experiment.ts @@ -57,11 +57,12 @@ import { QueryContext } from '../local/query_context'; export async function runPersistentCacheIndexPerformanceExperiment( log: (...args: unknown[]) => unknown, logLevel: 'info' | 'debug' -): Promise { +): Promise { const testObjects = await createTestObjects(); const experiment = new AutoIndexingExperiment(log, logLevel, testObjects); - await experiment.run(); + const heuristic = await experiment.run(); await testObjects.persistence.shutdown(); + return heuristic; } interface TestObjects { @@ -98,7 +99,7 @@ class AutoIndexingExperiment { this.documentOverlayCache = testObjects.documentOverlayCache; } - async run(): Promise { + async run(): Promise { // Every set contains 10 documents const numOfSet = 100; // could overflow. Currently it is safe when numOfSet set to 1000 and running on macbook M1 @@ -228,6 +229,12 @@ class AutoIndexingExperiment { `The time heuristic is ` + `${totalAfterIndex / totalResultCount} after auto indexing` ); + + return ( + totalAfterIndex / + totalResultCount / + (totalBeforeIndex / totalDocumentCount) + ); } async createTestingCollection( From 23729ba3a4548c38ba60a5a5360cd9ca21bf69e9 Mon Sep 17 00:00:00 2001 From: dconeybe Date: Tue, 5 Sep 2023 18:38:13 +0000 Subject: [PATCH 8/8] Update API reports --- common/api-review/firestore.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 4fcb0cf9e71..ea8a38c2fe0 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -585,7 +585,7 @@ export class QueryStartAtConstraint extends QueryConstraint { export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; // @public -export function runPersistentCacheIndexPerformanceExperiment(log: (...args: unknown[]) => unknown, logLevel: 'info' | 'debug'): Promise; +export function runPersistentCacheIndexPerformanceExperiment(log: (...args: unknown[]) => unknown, logLevel: 'info' | 'debug'): Promise; // @public export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise, options?: TransactionOptions): Promise;