-
-
Notifications
You must be signed in to change notification settings - Fork 101
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
Optimize cache subscriptions #1249
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'houdini': patch | ||
--- | ||
|
||
Optimize cache subscriptions |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,16 +22,21 @@ export class InMemorySubscriptions { | |
this.cache = cache | ||
} | ||
|
||
private subscribers: { | ||
[id: string]: { [fieldName: string]: FieldSelection[] } | ||
} = {} | ||
private referenceCounts: { | ||
[id: string]: { [fieldName: string]: Map<SubscriptionSpec['set'], number> } | ||
} = {} | ||
private subscribers = new Map< | ||
string, | ||
Map< | ||
string, | ||
{ | ||
selections: FieldSelection[] | ||
referenceCounts: Map<SubscriptionSpec['set'], number> | ||
} | ||
> | ||
>() | ||
|
||
private keyVersions: { [key: string]: Set<string> } = {} | ||
|
||
activeFields(parent: string): string[] { | ||
return Object.keys(this.subscribers[parent] || {}) | ||
return Object.keys(this.subscribers.get(parent) || {}) | ||
} | ||
|
||
add({ | ||
|
@@ -137,14 +142,23 @@ export class InMemorySubscriptions { | |
type: string | ||
}) { | ||
const spec = selection[0] | ||
|
||
// if we haven't seen the id or field before, create a list we can add to | ||
if (!this.subscribers[id]) { | ||
this.subscribers[id] = {} | ||
if (!this.subscribers.has(id)) { | ||
this.subscribers.set(id, new Map()) | ||
} | ||
if (!this.subscribers[id][key]) { | ||
this.subscribers[id][key] = [] | ||
|
||
const subscriber = this.subscribers.get(id)! | ||
|
||
if (!subscriber.has(key)) { | ||
subscriber.set(key, { | ||
selections: [], | ||
referenceCounts: new Map(), | ||
}) | ||
} | ||
|
||
const subscriberField = subscriber.get(key)! | ||
|
||
// if this is the first time we've seen the raw key | ||
if (!this.keyVersions[key]) { | ||
this.keyVersions[key] = new Set() | ||
|
@@ -153,21 +167,15 @@ export class InMemorySubscriptions { | |
// add this version of the key if we need to | ||
this.keyVersions[key].add(key) | ||
|
||
if (!this.subscribers[id][key].map(([{ set }]) => set).includes(spec.set)) { | ||
this.subscribers[id][key].push([spec, selection[1]]) | ||
if (!subscriberField.selections.some(([{ set }]) => set === spec.set)) { | ||
subscriberField.selections.push([spec, selection[1]]) | ||
} | ||
|
||
// if this is the first time we've seen this key | ||
if (!this.referenceCounts[id]) { | ||
this.referenceCounts[id] = {} | ||
} | ||
if (!this.referenceCounts[id][key]) { | ||
this.referenceCounts[id][key] = new Map() | ||
} | ||
const counts = this.referenceCounts[id][key] | ||
|
||
// we're going to increment the current value by one | ||
counts.set(spec.set, (counts.get(spec.set) || 0) + 1) | ||
subscriberField.referenceCounts.set( | ||
spec.set, | ||
(subscriberField.referenceCounts.get(spec.set) || 0) + 1 | ||
) | ||
|
||
// reset the lifetime for the key | ||
this.cache._internal_unstable.lifetimes.resetLifetime(id, key) | ||
|
@@ -293,7 +301,7 @@ export class InMemorySubscriptions { | |
} | ||
|
||
get(id: string, field: string): FieldSelection[] { | ||
return this.subscribers[id]?.[field] || [] | ||
return this.subscribers.get(id)?.get(field)?.selections || [] | ||
} | ||
|
||
remove( | ||
|
@@ -346,18 +354,16 @@ export class InMemorySubscriptions { | |
|
||
reset() { | ||
// Get all subscriptions that do not start with the rootID | ||
const subscribers = Object.entries(this.subscribers).filter( | ||
([id]) => !id.startsWith(rootID) | ||
) | ||
const subscribers = [...this.subscribers.entries()].filter(([id]) => !id.startsWith(rootID)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, doesn't same with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So So while |
||
|
||
// Remove those subcribers from this.subscribers | ||
for (const [id, _fields] of subscribers) { | ||
delete this.subscribers[id] | ||
this.subscribers.delete(id) | ||
} | ||
|
||
// Get list of all SubscriptionSpecs of subscribers | ||
const subscriptionSpecs = subscribers.flatMap(([_id, fields]) => | ||
Object.values(fields).flatMap((field) => field.map(([spec]) => spec)) | ||
[...fields.values()].flatMap((field) => field.selections.map(([spec]) => spec)) | ||
) | ||
|
||
return subscriptionSpecs | ||
|
@@ -368,12 +374,16 @@ export class InMemorySubscriptions { | |
// checking reference counts | ||
let targets: SubscriptionSpec['set'][] = [] | ||
|
||
const subscriber = this.subscribers.get(id) | ||
const subscriberField = subscriber?.get(fieldName) | ||
|
||
for (const spec of specs) { | ||
const counts = subscriber?.get(fieldName)?.referenceCounts | ||
|
||
// if we dont know this field/set combo, there's nothing to do (probably a bug somewhere) | ||
if (!this.referenceCounts[id]?.[fieldName]?.has(spec.set)) { | ||
if (!counts?.has(spec.set)) { | ||
continue | ||
} | ||
const counts = this.referenceCounts[id][fieldName] | ||
const newVal = (counts.get(spec.set) || 0) - 1 | ||
|
||
// decrement the reference of every field | ||
|
@@ -387,8 +397,8 @@ export class InMemorySubscriptions { | |
} | ||
|
||
// we do need to remove the set from the list | ||
if (this.subscribers[id]) { | ||
this.subscribers[id][fieldName] = this.get(id, fieldName).filter( | ||
if (subscriberField) { | ||
subscriberField.selections = this.get(id, fieldName).filter( | ||
([{ set }]) => !targets.includes(set) | ||
) | ||
} | ||
|
@@ -397,17 +407,18 @@ export class InMemorySubscriptions { | |
removeAllSubscribers(id: string, targets?: SubscriptionSpec[], visited: string[] = []) { | ||
visited.push(id) | ||
|
||
const subscriber = this.subscribers.get(id) | ||
// every field that currently being subscribed to needs to be cleaned up | ||
for (const field of Object.keys(this.subscribers[id] || [])) { | ||
for (const [key, val] of subscriber?.entries() ?? []) { | ||
// grab the current set of subscribers | ||
const subscribers = targets || this.subscribers[id][field].map(([spec]) => spec) | ||
const subscribers = targets || val.selections.map(([spec]) => spec) | ||
|
||
// delete the subscriber for the field | ||
this.removeSubscribers(id, field, subscribers) | ||
this.removeSubscribers(id, key, subscribers) | ||
|
||
// look up the value for the field so we can remove any subscribers that existed because of a | ||
// subscriber to this record | ||
const { value, kind } = this.cache._internal_unstable.storage.get(id, field) | ||
const { value, kind } = this.cache._internal_unstable.storage.get(id, key) | ||
|
||
// if the field is a scalar, there's nothing more to do | ||
if (kind === 'scalar') { | ||
|
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.
Is this change the core of the improvement? I'm curious if you noticed much of an improvement going from
{}
to aMap
. I'm totally fine to keep it how you have it, just curiousThere 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.
To answer the
Map
part first, the performance should be 'better', but on a scale that probably isn't going to matter much. Using Map has advantages, but frankly, my main reason is that the Map api is just nicer to use (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps for a comparison)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 actual improvement comes from combining
map()
and.includes()
intosome()
.This is because those functions, unlike in let's say C# (IEnumerable) or Java (Stream), operate on arrays, so chaining methods can actually become quite expansive. The reason for that is that
map()
,filter()
and the like return a new array, so you end up allocating n arrays, with n being the amount of chained functions.This doesn't matter that much for small arrays, but for larger arrays it starts to add up.