apollo-client: Memory leak with optimistic response

When using mutations with an optimisticResponse we found a significant increase in memory usage.

Intended outcome: No or only small increase in memory usage.

Actual outcome: Huge increase in memory usage.

How to reproduce the issue: Given an application with multiple watched queries:

  • take a memory snapshot
  • perform a mutation with an optimistic response (it is sufficient to update a single field of some object)
  • take another memory snapshot

Alternatively use the memory profiling function of Chrome instead of taking separate snapshots - this has the advantage that we get callstacks for the allocations.

Versions

  • apollo-cache-inmemory: 1.3.8
  • apollo-client: 2.4.5
  • apollo-link: 1.2.3
  • apollo-link-context: 1.0.9
  • apollo-link-error: 1.1.1
  • apollo-link-http: 1.5.5
  • react-apollo: 2.2.4

We suspect that this was introduced in #3394. In particular I suspect the commit 45c4169fa9bf0b4c61e3273254e851e05905a431 to be the main cause.

If InMemoryCache.optimistic contains some entry, read and diff create temporary stores with that optimistic response: https://github.com/apollographql/apollo-client/blob/48a224d504487ea480bce95685402d40c4a90eea/packages/apollo-cache-inmemory/src/inMemoryCache.ts#L137-L139 https://github.com/apollographql/apollo-client/blob/48a224d504487ea480bce95685402d40c4a90eea/packages/apollo-cache-inmemory/src/inMemoryCache.ts#L167-L169 These temporary stores are than used in the calls to readQueryFromStore/ diffQueryAgainstStore and should later get garbage collected. But unfortunately these store objects are internally used as key in calls to cacheKeyRoot.lookup: https://github.com/apollographql/apollo-client/blob/48a224d504487ea480bce95685402d40c4a90eea/packages/apollo-cache-inmemory/src/readFromStore.ts#L124-L130 https://github.com/apollographql/apollo-client/blob/48a224d504487ea480bce95685402d40c4a90eea/packages/apollo-cache-inmemory/src/readFromStore.ts#L144-L150 45c4169fa9bf0b4c61e3273254e851e05905a431 replaced the defaultMakeCacheKey function from optimism (which internally uses a WeakMap) with the newly introduced CacheKeyNode concept, causing these temporary stores to end up as key in one of the (strong) Maps, effectively leaking them.

The more watched queries you have, the bigger the problem because maybeBroadcastWatch calls diff for every single watcher. So we end up creating (and leaking) a temporary store for every single watcher for a single optimistic response (apart from the leakage, there is certainly some room for optimization here).

Not sure if this is the only problem or if there are other leaks as well. We saw a single mutation (that updates a single field) to cause a memory increase of 40-80MBs - not sure if such a huge increase can be caused by this problem alone.

@benjamn Since you were the main author of #3394, could you take a look at this? Let me know if you need more information!

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 18
  • Comments: 19 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Ok, apollo-cache-inmemory@1.3.12 has just been published to npm. Closing this issue now, perhaps somewhat optimistically, because I believe the memory leaks that were specifically related to optimistic responses have been addressed. Please open new issues for any other memory problems. Thanks!

O.k., tested with optimistic response. Here comes the test of clicking an option repeatedly and watching memory usage:

  • Without optimistic response: Memory rises, beginning at 70 MB. Stopps rising and drops a little at 400 MB after about 20 clicks. After some inactivity falls back to 70MB.
  • With optimistic response, without this update: Memory keeps rising, beginning at 250 MB. App crashing at 2GB after 80 clicks.
  • With optimistic response, with this update: Memory keeps rising, beginning at 70 MB. Does not seem to rise over 350 MB. After some inactivity falls back to 70MB.

The difference between using optimistic response and not may be accidental, as the numbers vary. But the update definitely seems to solve the crashing when using optimistic response in my app.

As I suspected when we were originally working on this issue, optimistic cache reads could be implemented much more efficiently. This PR should improve the speed and memory consumption of optimistic reads and writes considerably: https://github.com/apollographql/apollo-client/pull/4319

@benjamn Thanks for the clarification. I saw the commit to remove the WeakMap, but it wasn’t clear to me why this was necessary.

BTW: our tests also confirm that the memory issue is now fixed! 👍

@benjamn I was using directly InMemoryCache for a project, I was writing one big query and reading pieces using readFragments. With some very little load, my instance was quickly running out of memory because of memory spikes. With latest version, it does not.

Thanks for the quick fix on this, very very much appreciated from graphQL lovers out there

Tested this locally for Spectrum:

  • Without this update: started at 60mb, initiated 20 optimistic updates, ended at 1gb memory. Chart is continuous line up in heap memory, no apparent gc.
  • With this update: started at 60mb, initiated 20 optimistic updates, peaked at 304mb memory with very clear gc resets.