Skip to content

Keep track of number of queries in the query cache #510

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 61 commits into from
Feb 27, 2018
Merged

Conversation

gsoltis
Copy link

@gsoltis gsoltis commented Feb 13, 2018

  • Adds a targetCount to the global target metadata
  • Splits QueryCache add and update methods so that we can update the count appropriately.
  • Adds count() method to QueryCache to report the number of targets currently in the cache
  • Adds some schema migration infrastructure, partially courtesy of @schmidt-sebastian
  • Adds a schema migration to ensure that the global target metadata exists, and then updates it with the count of any targets that might already exist in the persistence layer.
  • Changes LocalStore to call updateQueryData instead of addQueryData in the appropriate place.

iOS counterpart: firebase/firebase-ios-sdk#776

@@ -97,36 +97,76 @@ export class IndexedDbQueryCache implements QueryCache {
);
}

private saveMetadata(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was gonna claim ownership of the term 'metadata' (but Mike already shut me down). Can you make this more query-specific (updateQueryMetadata) ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think metadata is fine, since it's a private method referring specifically to the instance-scoped field called metadata. I don't think it lays claim to an sdk-wide notion of metadata.

* Schema Version for the Web client:
* 1. Initial version including Mutation Queue, Query Cache, and Remote Document
* Cache
* 2. Added targetCount to targetGlobal row.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add note that this is needed for GC?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is actually a good idea. It's the sort of comment that could easily become outdated by accident, since whoever starts using it for something else would have no reason to check and update this location. I think it's better to stick to just describing what the schema updates are.

*
* @param {IDBTransaction} txn The version upgrade transaction for indexeddb
*/
function ensureTargetGlobal(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureTargetGlobalEntryExists ? The current name raises more questions than it answers :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to ensureTargetGlobalExists. The comment should also clarify any questions raised.

// createObjectStore(), etc. are synchronous.
// We are provided a version upgrade transaction from the request, so
// we wrap that in a SimpleDbTransaction to allow use of our friendlier
// API for schema migration operations.
const db = (event.target as IDBOpenDBRequest).result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do the wrapping right here? That way, your comment is immediately related to the next line.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -342,6 +360,14 @@ export class SimpleDbStore<KeyType extends IDBValidKey, ValueType> {
return wrapRequest<void>(request);
}

// if we ever need more of the count variants, we can add overloads. For now,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: /** Counts all objects in the store. Runs in O(n) */

In my opinion, The part about the overloads doesn't seem to warrant a comment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the part about overloads because we are explicitly not supporting part of the underlying API, whereas other methods in this class provide overloads to handle all of the variants of the API calls that they are wrapping. As this is a deviation from what we are currently do, I thought it warranted a comment.

I added the O(n) note though.

@@ -47,6 +47,17 @@ describe('MemoryQueryCache', () => {
});

genericQueryCacheTests();

it('cannot remove nonexistent query', async () => {
// We don't check on indexeddb query cache, but we do for memory.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

} catch {
gotError = true;
}
expect(gotError).to.be.true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this should only be true for MemoryPersistence?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I updated the comment.

// We don't check on indexeddb query cache, but we do for memory.
let gotError = false;
try {
await cache.removeQueryData(testQueryData(QUERY_ROOMS, 1, 1));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just fail() here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@gsoltis
Copy link
Author

gsoltis commented Feb 16, 2018

Ok, I think I have addressed everything now.

Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay. This basically LGTM but I found a few remaining minor comments / requests, and then this should be good to go.

@@ -97,36 +97,79 @@ export class IndexedDbQueryCache implements QueryCache {
);
}

private saveMetadata(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this down with the other private helper methods? (probably next to saveQueryData)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

// TODO(gsoltis): add sequence number check
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I think this is much clearer!

db.createObjectStore(DbTargetGlobal.store);
let p = PersistencePromise.resolve();
if (fromVersion < 2 && toVersion >= 2) {
p = p.next(() => ensureTargetGlobal(txn)).next(() => saveTargetCount(txn));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the rename... I also still don't think there's a need to have two steps here. TargetGlobal was an existing part of the schema in v1. The only thing you added was a new field to it.

Another way to deal with this would be to get rid of ensureTargetGlobal() and have populateTargetCount() just do nothing if the TargetGlobal row doesn't exist yet (since it should get populated correctly as 0 when the TargetGlobal row gets created as a normal part of the client operating). And actually, I think I like this better since 1) it avoids us duplicating the default new DbTargetGlobal( ... ) code.

* Counts the number of targets persisted and adds that value to the target
* global singleton.
*
* @param {IDBTransaction} txn The version upgrade transaction for indexeddb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc type annotation is unnecessary and no longer correct. :-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted.

);
const targetStore = txn.store<DbTargetKey, DbTarget>(DbTarget.store);
return targetStore.count().next(count => {
return globalStore.get(DbTargetGlobal.key).next(metadata => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadata is nullable here and so you should be getting an error for not handling it, but unfortunately we are not compiling with strictNullChecks (this was an accident when we migrated from google3 to github; b/73018483 tracks turning it on). In an effort to avoid adding additional debt, can you handle null here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it to actually thread the metadata through, so it should not be null anymore.


constructor(private transaction: IDBTransaction) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private readonly if you want...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// all we need is to count everything in a store.
//
// Returns the number of rows in the store. Execution time, off of the event
// loop, is O(# rows).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use /* ... */ style comment? And I'm not sure what "off of the event loop" is referring to. I would probably drop it unless there's something special about this method compared to other methods on this class...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The execution time is O(n), but the execution occurs on a different thread, so it does not block the event loop. It just means the amount of time until the you get the callback is proportional to the number of items that need to be counted. It was previously requested that I note that it was O(n). I don't feel too strongly either way...

For now I switched the comment style and reworded it slightly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no threads in JavaScript [well, excluding web workers] and everything runs on the event loop. So if you want to say something, you could say, "Execution time is O(# rows), although asynchronous." But since it returns a PersistencePromise, it's probably not worth calling out that it's asynchronous.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, yes, there are not multiple threads running js code, but the VM has multiple threads, and it appears that at least in chrome, indexeddb gets its own thread.

Regardless, I dropped the comment about running time. If anyone wants it back, they can send a PR.

// memory. So, this test is specific to the memory-backed query cache.
try {
await cache.removeQueryData(testQueryData(QUERY_ROOMS, 1, 1));
fail('should not have been able to remove nonexistent query');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fail method just throws an exception, which you'll then swallow... and so I think this test can't fail. You probably want expect.fail() instead, which hooks into the test runner to register the failure rather than relying on an exception.

You could also consider writing it without await, since .then() lets you attach both a success and rejection handler at once:

return cache.removeQueryData(...).then(
  () => fail(' ... '),
  (err) => { /* expected. */ });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively I don't feel strongly that we need tests to cover assertions and since you didn't add tests for updateQueryData() or addQueryData(), I'd be somewhat inclined to just drop this test. If you do that, please undo the QUERY_* / testQueryData() refactor.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

@@ -68,35 +80,35 @@ describe('IndexedDbQueryCache', () => {
genericQueryCacheTests();
});

const QUERY_ROOMS = Query.atPath(path('rooms'));
const QUERY_HALLS = Query.atPath(path('halls'));
const QUERY_GARAGES = Query.atPath(path('garages'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that these are file-level globals can you move them to the top of the file (above persistence and cache)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undid this change.

const updated = testQueryData(QUERY_ROOMS, 1, 2);
await cache.updateQueryData(updated);
const retrieved = await cache.getQueryData(updated.query);
expect(retrieved).to.deep.equal(updated);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, setAndReadQuery() is only used once and therefore doesn't justify itself as a helper method. Can you inline the single usage?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@mikelehen mikelehen assigned gsoltis and unassigned mikelehen Feb 21, 2018
@gsoltis
Copy link
Author

gsoltis commented Feb 21, 2018

Ok, I can't seem to reply to the comment about not seeing the ensureTargetGlobalExists rename, because github now thinks it's outdated. Anyways, I have renamed it, and it remains necessary because schema 0 does create the object store, but it does not create the row. We need the row to exist before updating the target count, although in that case we are guaranteed that the count is 0. I would rather handle ensuring the row exists in schema migrations. Then, the rest of the code can treat it as guaranteed-to-exist. I've added an assert that it exists in the query cache, since the query cache code gets called after the migration code. This also matches what we do for iOS: https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/Source/Local/FSTLevelDBQueryCache.mm#L96

Otherwise, I think it's weird to do some db initialization in migrations, and some in constructors / start methods.

@gsoltis gsoltis assigned mikelehen and unassigned gsoltis Feb 21, 2018
Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with 2 optional nits.

FYI- You can (I think) always reply to a file comment in github by clicking the src/packages/firestore/... link directly above the comment which will take you to the version of the code as-of that comment. It's certainly annoying though.

@@ -56,7 +55,8 @@ export class IndexedDbQueryCache implements QueryCache {
private metadata = new DbTargetGlobal(
/*highestTargetId=*/ 0,
/*lastListenSequenceNumber=*/ 0,
SnapshotVersion.MIN.toTimestamp()
SnapshotVersion.MIN.toTimestamp(),
/*targetCount=*/ 0
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you've reworked this so that the DBTargetGlobal always exists, I think we don't need to initialize metadata here, and I'd prefer not have these defaults duplicated unnecessarily between here and indexeddb_schema.ts. So perhaps change this to just:

private metadata: DbTargetGlobal = null;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and fixed the bug in the tests where we weren't calling start() but didn't know because we were initializing this to a dummy value.

So, good suggestion!

// all we need is to count everything in a store.
//
// Returns the number of rows in the store. Execution time, off of the event
// loop, is O(# rows).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no threads in JavaScript [well, excluding web workers] and everything runs on the event loop. So if you want to say something, you could say, "Execution time is O(# rows), although asynchronous." But since it returns a PersistencePromise, it's probably not worth calling out that it's asynchronous.

@gsoltis
Copy link
Author

gsoltis commented Feb 23, 2018

I'm OOO Friday and Monday, so I'm going to wait until Tuesday to merge this, on the off chance that something breaks.

@mikelehen mikelehen assigned gsoltis and unassigned mikelehen Feb 23, 2018
@gsoltis gsoltis merged commit 9cda9c9 into master Feb 27, 2018
@gsoltis gsoltis deleted the add_query_count branch February 27, 2018 17:48
@firebase firebase locked and limited conversation to collaborators Oct 23, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants