-
Notifications
You must be signed in to change notification settings - Fork 10
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
Loadtest E2Es and monitor memory for leak detection #611
Draft
enisdenjo
wants to merge
19
commits into
main
Choose a base branch
from
memtest
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
d9c8ac8
loadtest
enisdenjo 45b2439
no url and slope length
enisdenjo b21a81c
fix loadtest script
enisdenjo 9f0a54d
simplify
enisdenjo 526bf76
internal perf
enisdenjo 82a93ae
memtest that actually runs the test
enisdenjo 44ea292
nicer message
enisdenjo 546c66f
cleanup
enisdenjo 1f26ff3
workflow
enisdenjo b481390
no pipe logs
enisdenjo 304dbac
install k6
enisdenjo dd04515
skip memtests from examples
enisdenjo 4ac2cb6
simplify bench.yml
enisdenjo 8874c95
loadtest duration minute
enisdenjo 77ce4d4
allow more time for test teardown
enisdenjo 636918c
stricter threshold
enisdenjo 2275baf
stop loadtest if server exits
enisdenjo e8d0474
server exited before loadtest is fail
enisdenjo b491f0f
container wait for exit
enisdenjo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,6 @@ concurrency: | |
|
||
env: | ||
NODE_NO_WARNINGS: 1 | ||
CI: true | ||
|
||
jobs: | ||
bench: | ||
|
@@ -22,7 +21,7 @@ jobs: | |
- 10 | ||
- 100 | ||
- 1000 | ||
name: Benchmark / ${{matrix.e2e_runner}} / ${{matrix.products_size}} items | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks nicer in the checks overview. - Bench / Benchmark / node / 10 items
+ Bench / node / 10 items |
||
name: ${{matrix.e2e_runner}} / ${{matrix.products_size}} items | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
|
@@ -36,4 +35,3 @@ jobs: | |
env: | ||
PRODUCTS_SIZE: ${{matrix.products_size}} | ||
E2E_GATEWAY_RUNNER: ${{matrix.e2e_runner}} | ||
CI: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
name: Memtest | ||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
env: | ||
NODE_NO_WARNINGS: 1 | ||
K6_VERSION: v0.56.0 | ||
|
||
jobs: | ||
memtest: | ||
strategy: | ||
matrix: | ||
e2e_runner: [node, bun] | ||
name: ${{matrix.e2e_runner}} | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Install k6 | ||
run: | | ||
mkdir -p "$HOME/.local/bin" | ||
cd "$HOME/.local/bin" | ||
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 | ||
echo "$PWD" >> $GITHUB_PATH | ||
- name: Set up env | ||
uses: the-guild-org/shared-config/setup@v1 | ||
|
||
with: | ||
node-version-file: .node-version | ||
- name: Test | ||
run: yarn test:mem | ||
env: | ||
E2E_GATEWAY_RUNNER: ${{matrix.e2e_runner}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { createExampleSetup, createTenv } from '@internal/e2e'; | ||
import { memtest } from '@internal/perf/memtest'; | ||
|
||
const cwd = __dirname; | ||
|
||
const { gateway } = createTenv(cwd); | ||
const { supergraph, query } = createExampleSetup(cwd); | ||
|
||
memtest( | ||
{ | ||
cwd, | ||
query, | ||
}, | ||
async () => | ||
gateway({ | ||
supergraph: await supergraph(), | ||
}), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"name": "@internal/perf", | ||
"type": "module", | ||
"private": true, | ||
"main": "./src/index.ts", | ||
"dependencies": { | ||
"parse-duration": "^2.0.0", | ||
"regression": "^2.0.1" | ||
}, | ||
"devDependencies": { | ||
"@types/k6": "^0.54.2", | ||
"@types/regression": "^2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './loadtest'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { check } from 'k6'; | ||
import { test } from 'k6/execution'; | ||
import http from 'k6/http'; | ||
|
||
export default function () { | ||
const url = __ENV['URL']; | ||
if (!url) { | ||
return test.abort('Environment variable "URL" not provided'); | ||
} | ||
|
||
const query = __ENV['QUERY']; | ||
if (!query) { | ||
return test.abort('Environment variable "QUERY" not provided'); | ||
} | ||
|
||
const res = http.post(url, { query }); | ||
|
||
check(res, { | ||
'status is 200': (res) => res.status === 200, | ||
'body contains data': (res) => !!res.body?.toString().includes('"data":{'), | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import path from 'path'; | ||
import { setTimeout } from 'timers/promises'; | ||
import { ProcOptions, Server, spawn } from '@internal/proc'; | ||
import { trimError } from '@internal/testing'; | ||
|
||
export interface LoadtestOptions extends ProcOptions { | ||
cwd: string; | ||
/** @default 100 */ | ||
vus?: number; | ||
/** Duration of the loadtest in milliseconds. */ | ||
duration: number; | ||
/** | ||
* The snapshotting window of the GraphQL server memory in milliseconds. | ||
* | ||
* @default 1_000 | ||
*/ | ||
memorySnapshotWindow?: number; | ||
/** The GraphQL server on which the loadtest is running. */ | ||
server: Server; | ||
/** | ||
* The GraphQL query to execute for the loadtest. | ||
*/ | ||
query: string; | ||
} | ||
|
||
export async function loadtest(opts: LoadtestOptions) { | ||
const { | ||
cwd, | ||
vus = 100, | ||
duration, | ||
memorySnapshotWindow = 1_000, | ||
server, | ||
query, | ||
...procOptions | ||
} = opts; | ||
|
||
if (duration < 3_000) { | ||
throw new Error(`Duration has to be at least 3s, got "${duration}"`); | ||
} | ||
|
||
const ctrl = new AbortController(); | ||
const signal = AbortSignal.any([ | ||
ctrl.signal, | ||
AbortSignal.timeout( | ||
duration + | ||
// allow 1s for the k6 process to exit gracefully | ||
1_000, | ||
), | ||
]); | ||
|
||
const [, waitForExit] = await spawn( | ||
{ | ||
cwd, | ||
...procOptions, | ||
signal, | ||
}, | ||
'k6', | ||
'run', | ||
`--vus=${vus}`, | ||
`--duration=${duration}ms`, | ||
`--env=URL=${server.protocol}://localhost:${server.port}/graphql`, | ||
`--env=QUERY=${query}`, | ||
path.join(__dirname, 'loadtest-script.ts'), | ||
); | ||
using _ = { | ||
[Symbol.dispose]() { | ||
ctrl.abort(); | ||
}, | ||
}; | ||
|
||
const memoryInMBSnapshots: number[] = []; | ||
|
||
// we dont use a `setInterval` because the proc.getStats is async and we want stats ordered by time | ||
(async () => { | ||
while (!signal.aborted) { | ||
await setTimeout(memorySnapshotWindow); | ||
try { | ||
const { mem } = await server.getStats(); | ||
memoryInMBSnapshots.push(mem); | ||
} catch (err) { | ||
if (!signal.aborted) { | ||
throw err; | ||
} | ||
return; // couldve been aborted after timeout or while waiting for stats | ||
} | ||
} | ||
})(); | ||
|
||
await Promise.race([ | ||
waitForExit, | ||
server.waitForExit.then(() => { | ||
throw new Error( | ||
`Server exited before the loadtest finished\n${trimError(server.getStd('both'))}`, | ||
); | ||
}), | ||
]); | ||
|
||
return { memoryInMBSnapshots }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { Server } from '@internal/proc'; | ||
import regression from 'regression'; | ||
import { it } from 'vitest'; | ||
import { loadtest, LoadtestOptions } from './loadtest'; | ||
|
||
export interface MemtestOptions | ||
extends Omit<LoadtestOptions, 'duration' | 'server'> { | ||
/** | ||
* Duration of the loadtest in milliseconds. | ||
* | ||
* @default 60_000 | ||
*/ | ||
duration?: number; | ||
/** | ||
* Linear regression line slope threshold of the memory snapshots. | ||
* If the slope is greater than this value, the test will fail. | ||
* | ||
* @default 5 | ||
*/ | ||
memoryThresholdInMB?: number; | ||
} | ||
|
||
export function memtest(opts: MemtestOptions, setup: () => Promise<Server>) { | ||
const { memoryThresholdInMB = 5, duration = 60_000, ...loadtestOpts } = opts; | ||
it( | ||
'should not have a memory increase trend', | ||
{ | ||
timeout: duration + 10_000, // allow 10s for the test teardown | ||
}, | ||
async ({ expect }) => { | ||
const server = await setup(); | ||
|
||
const { memoryInMBSnapshots } = await loadtest({ | ||
...loadtestOpts, | ||
duration, | ||
server, | ||
}); | ||
|
||
const slope = calculateRegressionSlope(memoryInMBSnapshots); | ||
|
||
expect( | ||
slope, | ||
`Memory increase trend detected with slope of ${slope}MB (exceding threshold of ${memoryThresholdInMB}MB)`, | ||
).toBeLessThan(memoryThresholdInMB); | ||
}, | ||
); | ||
} | ||
|
||
/** | ||
* Detects a memory increase trend in an array of memory snapshots over time using linear regression. | ||
* | ||
* @param snapshots - An array of memory snapshots in MB. | ||
* | ||
* @returns The slope of the linear regression line. | ||
*/ | ||
function calculateRegressionSlope(snapshots: number[]) { | ||
if (snapshots.length < 2) { | ||
throw new Error('Not enough snapshots to determine trend'); | ||
} | ||
|
||
const data: [x: number, y: number][] = snapshots.map((memInMB, timestamp) => [ | ||
timestamp, | ||
memInMB, | ||
]); | ||
const result = regression.linear(data); | ||
const slope = result.equation[0]; | ||
if (!slope) { | ||
throw new Error('Regression slope is zero'); | ||
} | ||
|
||
return slope; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub sets
CI=true
since 2020.