-
Notifications
You must be signed in to change notification settings - Fork 929
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
Conversation
@@ -97,36 +97,76 @@ export class IndexedDbQueryCache implements QueryCache { | |||
); | |||
} | |||
|
|||
private saveMetadata( |
There was a problem hiding this comment.
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) ?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Ok, I think I have addressed everything now. |
There was a problem hiding this 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( |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. :-)
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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. */ });
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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')); |
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Ok, I can't seem to reply to the comment about not seeing the Otherwise, I think it's weird to do some db initialization in migrations, and some in constructors / start methods. |
There was a problem hiding this 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 | |||
); |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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.
I'm OOO Friday and Monday, so I'm going to wait until Tuesday to merge this, on the off chance that something breaks. |
targetCount
to the global target metadataQueryCache
add
andupdate
methods so that we can update the count appropriately.count()
method toQueryCache
to report the number of targets currently in the cacheLocalStore
to callupdateQueryData
instead ofaddQueryData
in the appropriate place.iOS counterpart: firebase/firebase-ios-sdk#776