apollographql / apollo-link-state

✨ Manage your application's state with Apollo!
MIT License
1.4k stars 101 forks source link

Memory leak in removeClientSetsFromDocument #318

Open AndreyChechel opened 5 years ago

AndreyChechel commented 5 years ago

We've identified memory leak in removeClientSetsFromDocument function. The problem is the query parameter can refer to identical DocumentNodes which are actually different objects having different references. That makes cache hits impossible and results in the removed Map growing constantly.

Seems the problem relates to apollographql/apollo-client#3444 - this PR addresses absolutely the same issue in apollo-client repository.

Steps to reproduce

Our setup looks as the following - we have 2 servers: a Web server and an API server. The Web server communicates to the API server using Apollo client, the Web server also uses apollo-link-state to maintain some cache. Apollo client, cache object, links are created and initialized on the Web server for each client request and supposed to be fully released after a request is processed.

I prepared a small demo reproducing the problem:

  1. Create an empty project
  2. Add dependencies to package.json and run yarn install
    "apollo-cache-inmemory": "^1.3.6",
    "apollo-client": "^2.4.3",
    "apollo-link-state": "^0.4.2",
    "express": "^4.16.4",
    "graphql": "^14.0.2",
    "graphql-tag": "^2.10.0"
  3. Add index.js file
    
    const express = require("express");
    const gql = require("graphql-tag");
    const ApolloClient = require("apollo-client").ApolloClient;
    const InMemoryCache = require("apollo-cache-inmemory").InMemoryCache;
    const withClientState = require("apollo-link-state").withClientState;

// Create Express server var app = express();

app.get('/', async function (req, res) {

// Cache object is unique for each request
const cache = new InMemoryCache();

// Initialize link state
const link = withClientState({
    cache,
    defaults: {
        title: "",
    },
    resolvers: {
        Mutation: {
            updateTitle: (_, { title }, { cache }) => {
                const data = { title };
                cache.writeData({ data });
                return null;
            },
        },
    },
});

// Initialize GraphQL client
const client = new ApolloClient({
    cache,
    link
});

// Update App title in the local cache
await client.mutate({
    variables: { title: "Test App" },
    mutation: gql`
        mutation UpdateTitle($title: String!) {
            updateTitle(title: $title) @client {
                title
            }
        }`,
});

// Respond to the client. All allocated 
// resources are supposed to be released once done.
res.sendStatus(200);

});

// Start the server app.listen(3000, function () { });



4. Run the application `node --inspect=0.0.0.0:9229 ./index.js` 
5. Use Chrome DevTools to attach to NodeJS process
6. Load http://localhost:3000 page in a browser
7. Take a heap snapshot in DevTools (you might want to run GC before)
8. Reload the page
9. Repeat steps 7-8 a few times
10. Inspect snapshots, you'll notice Array objects increased and retained for each page reload
![image](https://user-images.githubusercontent.com/16582701/47431406-67bf5180-d7a4-11e8-80a2-ca74ef330228.png)
11. Look for a Map object named **removed** in Retainers panel. That Map has as many items, as many times the page was loaded. These items will never be removed - it's basically the reason of the memory leak issue.
![image](https://user-images.githubusercontent.com/16582701/47431614-e6b48a00-d7a4-11e8-8a00-dbcbc30f75fa.png)