apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.4k stars 2.66k forks source link

JS Heap grows when using subscriptions #6880

Closed H-a-w-k closed 11 months ago

H-a-w-k commented 4 years ago

Intended outcome:

When I subscribe, I expect the memory to stay somewhat constant, since the subscription only updates data, and does not add more elements.

Actual outcome:

As soon as I use useSubscription or subscribeToMore the js heap starts building up.

When using the apollo dev tools, I can see that the cache is not growing, and it behaves as expected. It's like the garbage collector doesn't get everything, and somewhere a the subscribed data is persisted.

This simple subscription causes the problem. To get a bigger leak, I've added a picture where I have used the subscription many times.

A repo with the test files can be found here: https://github.com/H-a-w-k/apollo-subscription-heap-growing

Client code:

import React, { useEffect } from "react";
import { WebSocketLink } from "@apollo/client/link/ws";
import {
  split,
  HttpLink,
  InMemoryCache,
  ApolloClient,
  gql,
  useQuery,
  ApolloProvider,
} from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";

const httpLink = new HttpLink({
  uri: "http://localhost:4000/",
});
const wsLink = new WebSocketLink({
  uri: "ws://localhost:4000/graphql",
  options: {
    reconnect: true,
  },
});

const link = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  wsLink,
  httpLink
);

const cache = new InMemoryCache({
  typePolicies: {
    Subscription: {
      fields: {
        booksUpdated: {
          merge: false,
        },
      },
    },
    Book: {
      keyFields: ["id"],
    },
  },
});

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

const query = gql`
  query booksQuery {
    books {
      id
      title
    }
  }
`;

const subscription = gql`
  subscription booksSubscription {
    books: booksUpdated {
      id
      title
    }
  }
`;

const MemoryLeakFinder = () => {
  const { data, subscribeToMore } = useQuery(query);

  useEffect(() => {
    subscribeToMore({
      document: subscription,
    });
    //Add for steeper curve
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    subscribeToMore({
      document: subscription,
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <p>Looking for leaks!! {data?.books?.[0]?.title}</p>;
};

function App() {
  return (
    <ApolloProvider client={client}>
      <MemoryLeakFinder></MemoryLeakFinder>
    </ApolloProvider>
  );
}

export default App;

Server code:

const { PubSub, ApolloServer, gql } = require("apollo-server");

const pubsub = new PubSub();
const typeDefs = gql`
  type Book {
    id: Int
    title: String
    author: String
  }
  type Query {
    books: [Book]
  }
  type Subscription {
    booksUpdated: [Book]
  }
`;
const books = [
  {
    id: 1,
    title: "Harry Potter and the Chamber of Secrets",
    author: "J.K. Rowling",
  },
  {
    id: 2,
    title: "Jurassic Park",
    author: "Michael Crichton",
  },
];

const BOOKS_UPDATED = "BOOKS_UPDATED";

let i = 1;

setInterval(() => {
  books[0].title = "Harry Potter and the Chamber of Secrets " + i++;
  pubsub.publish(BOOKS_UPDATED, { booksUpdated: books });
}, 500);

const resolvers = {
  Query: {
    books: () => books,
  },
  Subscription: {
    booksUpdated: {
      subscribe: () => pubsub.asyncIterator([BOOKS_UPDATED]),
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req, connection }) => {
    if (connection) {
      // check connection for metadata
      return connection.context;
    } else {
      // check from req
      const token = req.headers.authorization || "";

      return { token };
    }
  },
});

server.listen().then(({ url, subscriptionsUrl }) => {
  console.log(`🚀 Server ready at ${url}`);
  console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
});

How to reproduce the issue:

Use the repo with the test files can be found here: https://github.com/H-a-w-k/apollo-subscription-heap-growing

or just copy paste the code above and install necessary dependencies. Just start the client and server and start viewing the result in task manager and profiler

apollo cache 1 apollo cache 2 HeapFromTestProject

michal-damiecki commented 2 years ago

Hey @brainkim, any news about this issue? In our project, we've experienced the same problem.

etodanik commented 1 year ago

Experiencing the same exact thing on React Native, very severely

phryneas commented 1 year ago

I just did a 7 minute-sampling with the example given (but an update every 10ms, not 500, so I'd expect to see it even more), but I'm not seeing a significant heap size increase. I could understand this happening in React Native (as I already explained to @israelidanny in https://github.com/apollographql/apollo-client/issues/7775#issuecomment-1569695477 - please don't spam!), but I can't make sense of this in modern browsers.

@H-a-w-k, @michal-damiecki, could you give a bit more context on the environments where you were experiencing these issues?

phryneas commented 11 months ago

Since there hasn't been any more feedback for half a year and we cannot reproduce it ourselves, I'm closing the issue.

At the same time I want to bring it to your attention that the upcoming version 3.9 (out in beta right now) will contain a lot of memory fixes: See the announcement blog post.

If this comes up again (and has not been fixed by 3.9), please open a new issue with a new reproduction. Thank you!

github-actions[bot] commented 10 months ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. For general questions, we recommend using StackOverflow or our discord server.