Skip to content

Commit 523cb25

Browse files
Loadtest E2Es and monitor memory for leak detection (#611)
Co-authored-by: theguild-bot <[email protected]>
1 parent f8e1e39 commit 523cb25

File tree

41 files changed

+2973
-70
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2973
-70
lines changed

.github/workflows/bench.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ concurrency:
1111

1212
env:
1313
NODE_NO_WARNINGS: 1
14-
CI: true
1514

1615
jobs:
1716
bench:
@@ -22,7 +21,7 @@ jobs:
2221
- 10
2322
- 100
2423
- 1000
25-
name: Benchmark / ${{matrix.e2e_runner}} / ${{matrix.products_size}} items
24+
name: ${{matrix.e2e_runner}} / ${{matrix.products_size}} items
2625
runs-on: ubuntu-latest
2726
steps:
2827
- name: Checkout
@@ -36,4 +35,3 @@ jobs:
3635
env:
3736
PRODUCTS_SIZE: ${{matrix.products_size}}
3837
E2E_GATEWAY_RUNNER: ${{matrix.e2e_runner}}
39-
CI: true

.github/workflows/memtest.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Memtest
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
env:
13+
NODE_NO_WARNINGS: 1
14+
K6_VERSION: v0.56.0
15+
16+
jobs:
17+
memtest:
18+
strategy:
19+
matrix:
20+
e2e_runner:
21+
- node
22+
# - bun TODO: get memory snaps and heap sampling for bun. is it even necessary?
23+
name: ${{matrix.e2e_runner}}
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
- name: Install k6
29+
run: |
30+
mkdir -p "$HOME/.local/bin"
31+
cd "$HOME/.local/bin"
32+
curl https://github.com/grafana/k6/releases/download/${{ env.K6_VERSION }}/k6-${{ env.K6_VERSION }}-linux-amd64.tar.gz -L | tar xvz --strip-components 1
33+
echo "$PWD" >> $GITHUB_PATH
34+
- name: Set up env
35+
uses: the-guild-org/shared-config/setup@v1
36+
with:
37+
# TODO: should we test more node versions? we usually always recommend upgrading to
38+
# latest when people suspect leaks - latest is always the most stable
39+
node-version-file: .node-version
40+
- name: Test
41+
run: yarn test:mem
42+
# TODO: publish heap allocation sampling profile to artifact
43+
env:
44+
E2E_GATEWAY_RUNNER: ${{matrix.e2e_runner}}

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ jobs:
175175
uses: the-guild-org/shared-config/setup@v1
176176
with:
177177
node-version-file: .node-version
178+
# we skip-build on ubuntu arm because the "canvas" package wont build and there are no prebuilts
179+
install-command: ${{ matrix.os == 'ubuntu-24.04-arm' && 'yarn install --immutable --mode=skip-build' || '' }}
178180
- name: Build
179181
run: yarn build
180182
- name: Bundle

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ hive-gateway
1414
/examples/**/*/supergraph.graphql
1515
.helix/config.toml
1616
.helix/languages.toml
17+
/e2e/**/*/memtest-memory-usage_*.svg
18+
/e2e/**/*/*.heapsnapshot
19+
/e2e/**/*/*.heapprofile
20+
/e2e/**/*/loadtest.out

DEPS_RESOLUTIONS_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ Here we collect reasons and write explanations about why some resolutions or pat
1414
### vitest-tsconfig-paths
1515

1616
1. Resolve tsconfig paths in modules that have been [inlined](https://vitest.dev/config/#server-deps-inline).
17+
18+
### @memlab/core
19+
20+
1. Define package.json#export for `@memlab/core/Types`
21+
1. Define package.json#export for `@memlab/core/Utils`
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createTenv, type Container } from '@internal/e2e';
2+
import { memtest } from '@internal/perf/memtest';
3+
import { beforeAll } from 'vitest';
4+
5+
const cwd = __dirname;
6+
const { service, gateway, container } = createTenv(cwd);
7+
8+
let petstore!: Container;
9+
beforeAll(async () => {
10+
petstore = await container({
11+
name: 'petstore',
12+
image: 'swaggerapi/petstore3:1.0.7',
13+
containerPort: 8080,
14+
healthcheck: ['CMD-SHELL', 'wget --spider http://localhost:8080'],
15+
});
16+
});
17+
18+
memtest(
19+
{
20+
cwd,
21+
query: /* GraphQL */ `
22+
query GetPet {
23+
getPetById(petId: 1) {
24+
__typename
25+
id
26+
name
27+
vaccinated
28+
}
29+
}
30+
`,
31+
},
32+
async () =>
33+
gateway({
34+
supergraph: {
35+
with: 'mesh',
36+
services: [petstore, await service('vaccination')],
37+
},
38+
}),
39+
);

e2e/cloudflare-workers/cloudflare-workers.e2e.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,18 @@ describe.skipIf(gatewayRunner !== 'node')('Cloudflare Workers', () => {
7878
OTLP_SERVICE_NAME: string;
7979
}) {
8080
const port = await getAvailablePort();
81-
await spawn('yarn wrangler', {
82-
args: [
83-
'dev',
84-
'--port',
85-
port.toString(),
86-
'--var',
87-
'OTLP_EXPORTER_URL:' + env.OTLP_EXPORTER_URL,
88-
'--var',
89-
'OTLP_SERVICE_NAME:' + env.OTLP_SERVICE_NAME,
90-
...(isDebug() ? ['--var', 'DEBUG:1'] : []),
91-
],
92-
});
81+
await spawn([
82+
'yarn',
83+
'wrangler',
84+
'dev',
85+
'--port',
86+
port.toString(),
87+
'--var',
88+
'OTLP_EXPORTER_URL:' + env.OTLP_EXPORTER_URL,
89+
'--var',
90+
'OTLP_SERVICE_NAME:' + env.OTLP_SERVICE_NAME,
91+
...(isDebug() ? ['--var', 'DEBUG:1'] : []),
92+
]);
9393
const hostname = await getLocalhost(port);
9494
return {
9595
url: `${hostname}:${port}`,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { createExampleSetup, createTenv } from '@internal/e2e';
2+
import { memtest } from '@internal/perf/memtest';
3+
4+
const cwd = __dirname;
5+
6+
const { gateway } = createTenv(cwd);
7+
const { supergraph, query } = createExampleSetup(cwd);
8+
9+
memtest(
10+
{
11+
cwd,
12+
query,
13+
},
14+
async () =>
15+
gateway({
16+
supergraph: await supergraph(),
17+
}),
18+
);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createExampleSetup, createTenv } from '@internal/e2e';
2+
import { memtest } from '@internal/perf/memtest';
3+
4+
const cwd = __dirname;
5+
6+
const { service, gateway } = createTenv(cwd);
7+
const exampleSetup = createExampleSetup(cwd);
8+
9+
memtest(
10+
{
11+
cwd,
12+
query: exampleSetup.query,
13+
},
14+
async () =>
15+
gateway({
16+
supergraph: {
17+
with: 'mesh',
18+
services: [
19+
await service('accounts'),
20+
await exampleSetup.service('inventory'),
21+
await exampleSetup.service('products'),
22+
await exampleSetup.service('reviews'),
23+
],
24+
},
25+
}),
26+
);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { createTenv } from '@internal/e2e';
2+
import { memtest } from '@internal/perf/memtest';
3+
import { getAvailablePort } from '@internal/testing';
4+
import { describe } from 'vitest';
5+
6+
const cwd = __dirname;
7+
8+
const { service, gateway } = createTenv(cwd);
9+
10+
describe('upstream subscriptions via websockets', () => {
11+
memtest(
12+
{
13+
cwd,
14+
query: /* GraphQL */ `
15+
subscription {
16+
newProduct {
17+
id
18+
name
19+
price
20+
reviews {
21+
score
22+
}
23+
}
24+
}
25+
`,
26+
},
27+
async () =>
28+
gateway({
29+
supergraph: {
30+
with: 'apollo',
31+
services: [
32+
await service('products', { env: { MEMTEST: 1 } }),
33+
await service('reviews'),
34+
],
35+
},
36+
}),
37+
);
38+
});
39+
40+
describe('upstream subscriptions via http callbacks', () => {
41+
memtest(
42+
{
43+
cwd,
44+
query: /* GraphQL */ `
45+
subscription {
46+
newReview {
47+
score
48+
}
49+
}
50+
`,
51+
expectedHeavyFrame: (frame) =>
52+
// these frames are not leaks and have been confirmed to be stable analysing the heap snapshots (they do allocate a lot, but they all of their memory gets freed)
53+
[
54+
'delete',
55+
'get pathname',
56+
'onRequest',
57+
'Repeater.next',
58+
'Set',
59+
].includes(frame.name),
60+
},
61+
async () => {
62+
const availablePort = await getAvailablePort();
63+
const publicUrl = `http://0.0.0.0:${availablePort}`;
64+
return gateway({
65+
supergraph: {
66+
with: 'apollo',
67+
services: [
68+
await service('products', { env: { MEMTEST: 1 } }),
69+
await service('reviews'),
70+
],
71+
},
72+
port: availablePort,
73+
env: {
74+
PUBLIC_URL: publicUrl,
75+
},
76+
});
77+
},
78+
);
79+
});

0 commit comments

Comments
 (0)