Skip to content
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

perf: reduce memory usage upon running server tests #3949

Merged
merged 15 commits into from Aug 11, 2022

Conversation

apoorv-mishra
Copy link
Collaborator

@apoorv-mishra apoorv-mishra commented Aug 9, 2022

Closes #3939

This attempts to plug the memory leaks that occur upon running yarn test:server. I almost completely relied upon the --detect-leaks flag to spot and fix them. This flag gives room for the GC to sweep by awaiting for a few ticks in between.

Following are the results collected after making changes across the board:

node@16.16

$ node -v
v16.16.0

$ npx jest-heap-graph "yarn test:server --logHeapUsage --detect-leaks"

Test Suites: 120 passed, 120 total
Tests:       890 passed, 890 total
Snapshots:   86 passed, 86 total
Time:        335.578 s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

--- n: 120 ---
    1719.00 ┤                                                    ╭───────╮
    1568.60 ┤                                               ╭────╯       │
    1418.20 ┤                                          ╭────╯            │
    1267.80 ┤                                     ╭────╯                 │
    1117.40 ┤                                 ╭───╯                      │                       ╭────
     967.00 ┤                             ╭───╯                          │                  ╭────╯
     816.60 ┤                         ╭───╯                              │             ╭────╯
     666.20 ┤                     ╭───╯                                  │         ╭───╯
     515.80 ┤                 ╭───╯                                      │    ╭────╯
     365.40 ┼───╮╭─╮╭─────────╯                                          ╰────╯
     215.00 ┤   ╰╯ ╰╯

node@16.10

$ node -v
v16.10.0

$ npx jest-heap-graph "yarn test:server --logHeapUsage --detect-leaks"

Test Suites: 120 passed, 120 total
Tests:       890 passed, 890 total
Snapshots:   86 passed, 86 total
Time:        291.507 s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

--- n: 120 ---
     235.00 ┤                                      ╭╮
     228.40 ┤                                      ││╭╮                    ╭╮  ╭─╮
     221.80 ┤                                      ││││                  ╭─╯│╭─╯ │  ╭─╮
     215.20 ┤                        ╭╮            ││││             ╭╮╭──╯  ╰╯   ╰╮ │ │           ╭──╮
     208.60 ┤           ╭─╮          ││╭╮          │╰╯╰╮           ╭╯││           │ │ │╭─╮  ╭╮ ╭╮╭╯  │
     202.00 ┤           │ │╭╮        ││││   ╭╮   ╭─╯   │           │ ││           ╰╮│ ╰╯ ╰──╯╰─╯╰╯   │
     195.40 ┤           │ ╰╯╰╮ ╭╮╭─╮ ││││   │╰───╯     │    ╭╮     │ ││            ││                │
     188.80 ┤        ╭╮ │    │╭╯││ ╰╮│││╰─╮ │          ╰╮  ╭╯╰─────╯ ╰╯            ╰╯                ╰
     182.20 ┼────────╯╰─╯    ││ ╰╯  ││╰╯  │╭╯           ╰──╯
     175.60 ┤                ││     ╰╯    ││
     169.00 ┤                ╰╯           ╰╯

node@14.19

$ node -v
v14.19.3

$ npx jest-heap-graph "yarn test:server --logHeapUsage --detect-leaks"

Test Suites: 120 passed, 120 total
Tests:       890 passed, 890 total
Snapshots:   86 passed, 86 total
Time:        262.933 s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

--- n: 120 ---
     228.00 ┤                                                                                        ╭
     222.30 ┤                                                                        ╭╮   ╭──────────╯
     216.60 ┤                                                                ╭╮ ╭────╯╰───╯
     210.90 ┤                      ╭─╮╭╮╭╮╭╮ ╭─╮                     ╭╮ ╭────╯╰─╯
     205.20 ┤       ╭╮      ╭──────╯ ╰╯╰╯╰╯╰─╯ │             ╭╮╭─────╯╰─╯
     199.50 ┤       ││      │                  │╭─╮╭─────────╯╰╯
     193.80 ┤       ││      │                  ╰╯ ╰╯
     188.10 ┤      ╭╯│      │
     182.40 ┤╭╮╭╮╭╮│ ╰╮ ╭─╮ │
     176.70 ┼╯╰╯╰╯╰╯  ╰─╯ ╰╮│
     171.00 ┤              ╰╯


const server = getTestServer();
afterAll(() => disconnectdb().then(() => server.close()));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plugged up the leaks detected using --detect-leaks flag as shown here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently (as part of this issue) changed this to use a single centralized test server – the thinking was that it would be fast than spinning up an instance/dbconnection in each file. I'm actually pretty surprised this works though and the rest of the suite doesn't fail after the server is closed? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently (as part of this issue) changed this to use a single centralized test server – the thinking was that it would be fast than spinning up an instance/dbconnection in each file.

Looks like this is unreachable. Not sure why, likely due to how jest handles running multiple test files 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I'll admit I didn't explicitly check that. Must be a limitation of jest module scope, in that case how about we change getTestServer to the following:

export function getTestServer() {
  const app = webService();
  const testServer = new TestServer(app.callback());

  testServer.cleanup = async () => {
    await sequelize.close();
    await testServer.close();
  };

  return testServer;
}

Then we just need to add the following in individual test files

afterAll(server.cleanup);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right though, this cleanup makes a huge difference 🙏

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more we can centralize setup and teardown the less future similar mistakes, I'm okay centralizing – particularly as moving into the suites would also require importing sequelize

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about 👇

export function getTestServer() {
  const app = webService();
  const server = new TestServer(app.callback());

  server.disconnect = () => {
    await sequelize.close();
    await server.close();
  };

  return server;
}

export function getTestdb() {
  const db = sequelize;

  db.flush = () => { /* flush! */ };
  db.seed = () => { /* seed! */ };
  db.disconnect () => { /* disconnect! */ };
};

And,

// Usage(in server/routes/api/)

import { getTestServer, getTestdb } from "@server/test/utils"

const db = getTestdb();
const server = getTestServer();

afterAll(server.disconnect);

beforeEach(db.flush);
beforeEach(db.seed);

//////////////////////////////////////////////////////////////////////////

// Usage(in server/{commands, models...})

import { getTestdb } from "@server/test/utils";


const db = getTestdb();

afterAll(db.disconnect);

beforeEach(db.flush);
beforeEach(db.seed);

Copy link
Member

@tommoor tommoor Aug 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, seems clean and sensible 👍 . But getTestdb -> getTestDatabase

And don't include seed, I'm trying to gradually get rid of it in favor of individual factories.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And don't include seed, I'm trying to gradually get rid of it in favor of individual factories.

So, we keep seed as it is in current scope? Also, are you exploring an equivalent of factory_bot? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay – include seed if you're prepared to make the changes 😆

@@ -18,6 +18,7 @@ if (process.env.DATABASE_URL_TEST) {
require("@server/database/sequelize");

jest.mock("bull");
jest.mock("i18next-http-backend");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixed the leak for shared/i18n/index.test.ts.

@@ -1,8 +1,8 @@
import Abstract from "./Abstract";
import { URL_REGEX } from "./Abstract";
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The embeds were problematic in node@16.16.0. It occurred that maybe this would fix it since we don't really require the entire component for tests. I tried running it multiple times and the leaks came to halt, although I didn't spot considerable difference in heap usage.

On the other hand, in node@16.10.0 and node@14.19.3, these seemed to behave sanely with --detect-leaks flag.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, this is a painful change to make across all embeds though – and now there is an implicit requirement of both exports just for test compatibility.

I think that the /shared/ tests should just be split out into their own test:shared command that runs separately and if they're a touch leaky it at least won't affect the other server tests

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to split these out now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't have any conflicts, but heads up: #3951

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove these changes in embeds, otherwise you killed it – memory behavior is great now!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh..almost forgot! Thanks for pointing out. Reverted.

Copy link
Collaborator Author

@apoorv-mishra apoorv-mishra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just the sample changes for quick feedback. Once aligned, these changes would be propagated across all the tests.

@apoorv-mishra
Copy link
Collaborator Author

Ran into an issue after purging and re-pulling the postgres docker image 👇

● #documents.info › should return published document

    SequelizeConnectionError: database "outline-test" does not exist

      at Client._connectionCallback (node_modules/sequelize/src/dialects/postgres/connection-manager.js:189:24)
      at Client._handleErrorWhileConnecting (node_modules/pg/lib/client.js:305:19)
      at Client._handleErrorMessage (node_modules/pg/lib/client.js:325:19)
      at node_modules/pg/lib/connection.js:115:12
      at Parser.parse (node_modules/pg-protocol/src/parser.ts:102:9)
      at Socket.<anonymous> (node_modules/pg-protocol/src/index.ts:7:48)


  ● Test suite failed to run

    Server is not running.

Did you ever run into this issue @tommoor ?

@tommoor
Copy link
Member

tommoor commented Aug 10, 2022

You can run make test to recreate the test environment including db

@apoorv-mishra
Copy link
Collaborator Author

Shoot! How did I forget this 😆

server/test/support.ts Outdated Show resolved Hide resolved
Copy link
Collaborator Author

@apoorv-mishra apoorv-mishra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for tests under server/routes. I'll pause here for your review @tommoor so far 😆

@tommoor
Copy link
Member

tommoor commented Aug 10, 2022

No red flags!

@apoorv-mishra apoorv-mishra marked this pull request as ready for review August 11, 2022 06:12
@auto-assign auto-assign bot requested a review from tommoor August 11, 2022 06:12
@apoorv-mishra apoorv-mishra merged commit 0c51bfb into outline:main Aug 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Memory leak in test:server
2 participants