Skip to content

Commit 094df72

Browse files
committed
add loadtest workflow & script
1 parent b31910c commit 094df72

File tree

4 files changed

+330
-3
lines changed

4 files changed

+330
-3
lines changed

.github/workflows/load-test.yml

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: "Express Load Test"
2+
3+
permissions:
4+
contents: read
5+
pull-requests: write
6+
7+
on:
8+
pull_request:
9+
types: [ opened, synchronize ]
10+
workflow_dispatch:
11+
inputs:
12+
prev_branch:
13+
description: 'Base branch (branch-branch)'
14+
required: false
15+
default: ''
16+
curr_branch:
17+
description: 'Head branch (branch-branch)'
18+
required: false
19+
default: ''
20+
prev_version:
21+
description: 'Base Version (version-version)'
22+
required: false
23+
default: ''
24+
curr_version:
25+
description: 'Head Version (version-version)'
26+
required: false
27+
default: ''
28+
version:
29+
description: 'Version (version-branch)'
30+
required: false
31+
default: ''
32+
branch:
33+
description: 'Branch (version-branch)'
34+
required: false
35+
default: ''
36+
37+
jobs:
38+
load_test:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- name: Check Out Repository
42+
uses: actions/checkout@v4
43+
with:
44+
fetch-depth: 0
45+
46+
- name: Fetch All Branches
47+
run: git fetch --all
48+
49+
- name: Set Up Node.js
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version: '20'
53+
54+
- name: Determine Comparison Type
55+
run: |
56+
if [[ "${{ github.event_name }}" == "pull_request" || "${{ github.event_name }}" == "push" ]]; then
57+
# Branch comparison: Default to master for previous branch and use PR branch for current branch
58+
echo "PREV_BRANCH=master" >> $GITHUB_ENV
59+
echo "CURR_BRANCH=${{ github.head_ref || github.ref_name }}" >> $GITHUB_ENV
60+
elif [[ "${{ github.event.inputs.prev_branch }}" && "${{ github.event.inputs.curr_branch }}" ]]; then
61+
# Version comparison
62+
echo "PREV_BRANCH=${{ github.event.inputs.prev_branch }}" >> $GITHUB_ENV
63+
echo "CURR_BRANCH=${{ github.event.inputs.curr_branch }}" >> $GITHUB_ENV
64+
elif [[ "${{ github.event.inputs.prev_version }}" && "${{ github.event.inputs.curr_version }}" ]]; then
65+
# Version comparison
66+
echo "PREV_VERSION=${{ github.event.inputs.prev_version }}" >> $GITHUB_ENV
67+
echo "CURR_VERSION=${{ github.event.inputs.curr_version }}" >> $GITHUB_ENV
68+
elif [[ "${{ github.event.inputs.branch }}" && "${{ github.event.inputs.version }}" ]]; then
69+
# Branch-Version comparison
70+
echo "BRANCH=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
71+
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
72+
else
73+
echo "Invalid input combination. Provide either two branches, two versions, or one branch and one version."
74+
exit 1
75+
fi
76+
77+
- name: Install wrk
78+
run: |
79+
sudo apt-get update
80+
sudo apt-get install -y wrk
81+
82+
- name: Start Load Test Server
83+
run: node benchmarks/load-test-workflow.js
84+
env:
85+
PREV_BRANCH: ${{ env.PREV_BRANCH }}
86+
CURR_BRANCH: ${{ env.CURR_BRANCH }}
87+
PREV_VERSION: ${{ env.PREV_VERSION }}
88+
CURR_VERSION: ${{ env.CURR_VERSION }}
89+
BRANCH: ${{ env.BRANCH }}
90+
VERSION: ${{ env.VERSION }}
91+
92+
- name: Output Summary
93+
run: |
94+
cat benchmarks/results*.md >> $GITHUB_STEP_SUMMARY
95+
96+
- name: Post Summary to PR
97+
if: github.event_name == 'pull_request'
98+
run: |
99+
cat $GITHUB_STEP_SUMMARY
100+
gh pr comment ${{ github.event.pull_request.number }} --body "$(cat benchmarks/results*.md)"
101+
env:
102+
GH_TOKEN: ${{ github.token }}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ coverage
1212
# Benchmarking
1313
benchmarks/graphs
1414

15+
# Webstorm
16+
.idea
17+
1518
# ignore additional files using core.excludesFile
1619
# https://git-scm.com/docs/gitignore

benchmarks/load-test-workflow.js

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
const {execSync, spawn} = require('child_process')
2+
const fs = require('fs')
3+
4+
const runCommand = command => execSync(command, {encoding: 'utf8'}).trim()
5+
6+
const startServer = (middleware, isVersionTest) => {
7+
console.log(`Starting server with ${middleware} middleware layers...`)
8+
const server = spawn('node', ['benchmarks/middleware.js'], {
9+
env: {
10+
...process.env,
11+
MW: middleware,
12+
NO_LOCAL_EXPRESS: isVersionTest
13+
},
14+
stdio: 'inherit'
15+
})
16+
17+
return new Promise((resolve, reject) => {
18+
setTimeout(() => {
19+
try {
20+
execSync('curl -s http://127.0.0.1:3333')
21+
resolve(server)
22+
} catch (error) {
23+
server.kill()
24+
reject(new Error('Server failed to start.'))
25+
}
26+
}, 3000)
27+
})
28+
}
29+
30+
const runLoadTest = (url, connectionsList) => {
31+
return connectionsList.map(connections => {
32+
try {
33+
const output = runCommand(`wrk ${url} -d 3 -c ${connections} -t 8`)
34+
const reqSec = output.match(/Requests\/sec:\s+(\d+.\d+)/)?.[1]
35+
const latency = output.match(/Latency\s+(\d+.\d+)/)?.[1]
36+
return {connections, reqSec, latency}
37+
} catch (error) {
38+
console.error(
39+
`Error running load test for ${connections} connections:`,
40+
error.message
41+
)
42+
return {connections, reqSec: 'N/A', latency: 'N/A'}
43+
}
44+
})
45+
}
46+
47+
const generateMarkdownTable = results => {
48+
const headers = `| Connections | Requests/sec | Latency |\n|-------------|--------------|---------|`
49+
const rows = results
50+
.map(
51+
r => `| ${r.connections} | ${r.reqSec || 'N/A'} | ${r.latency || 'N/A'} |`
52+
)
53+
.join('\n')
54+
return `${headers}\n${rows}`
55+
}
56+
57+
const cleanUp = () => {
58+
console.log('Cleaning up...')
59+
runCommand('npm uninstall express')
60+
runCommand('rm -rf package-lock.json node_modules')
61+
}
62+
63+
const runTests = async ({
64+
identifier,
65+
connectionsList,
66+
middlewareCounts,
67+
outputFile,
68+
isVersionTest = false
69+
}) => {
70+
if (isVersionTest) {
71+
console.log(`Installing Express v${identifier}...`)
72+
runCommand(`npm install express@${identifier}`)
73+
} else {
74+
console.log(`Checking out branch ${identifier}...`)
75+
runCommand(`git fetch origin ${identifier}`)
76+
runCommand(`git checkout ${identifier}`)
77+
runCommand('npm install')
78+
console.log('Installing deps...')
79+
}
80+
81+
const resultsMarkdown = [
82+
`\n\n# Load Test Results for ${isVersionTest ? `Express v${identifier}` : `Branch ${identifier}`}`
83+
]
84+
85+
for (const middlewareCount of middlewareCounts) {
86+
try {
87+
const server = await startServer(middlewareCount, isVersionTest)
88+
const results = runLoadTest(
89+
'http://127.0.0.1:3333/?foo[bar]=baz',
90+
connectionsList
91+
)
92+
server.kill()
93+
resultsMarkdown.push(
94+
`### Load test for ${middlewareCount} middleware layers\n\n${generateMarkdownTable(results)}`
95+
)
96+
} catch (error) {
97+
console.error('Error in load test process:', error)
98+
}
99+
}
100+
101+
fs.writeFileSync(outputFile, resultsMarkdown.join('\n\n'))
102+
cleanUp()
103+
}
104+
105+
const compareBranches = async ({
106+
prevBranch,
107+
currBranch,
108+
connectionsList,
109+
middlewareCounts,
110+
}) => {
111+
console.log(`Comparing branches: ${prevBranch} vs ${currBranch}`)
112+
await runTests({
113+
identifier: prevBranch,
114+
connectionsList,
115+
middlewareCounts,
116+
outputFile: `benchmarks/results_${prevBranch}.md`,
117+
isVersionTest: false
118+
})
119+
await runTests({
120+
identifier: currBranch,
121+
connectionsList,
122+
middlewareCounts,
123+
outputFile: `benchmarks/results_${currBranch}.md`,
124+
isVersionTest: false
125+
})
126+
}
127+
128+
const compareVersions = async ({
129+
prevVersion,
130+
currVersion,
131+
connectionsList,
132+
middlewareCounts,
133+
}) => {
134+
console.log(
135+
`Comparing versions: Express v${prevVersion} vs Express v${currVersion}`
136+
)
137+
await runTests({
138+
identifier: prevVersion,
139+
connectionsList,
140+
middlewareCounts,
141+
outputFile: `benchmarks/results_${prevVersion}.md`,
142+
isVersionTest: true
143+
})
144+
await runTests({
145+
identifier: currVersion,
146+
connectionsList,
147+
middlewareCounts,
148+
outputFile: `benchmarks/results_${currVersion}.md`,
149+
isVersionTest: true
150+
})
151+
}
152+
153+
const compareBranchAndVersion = async ({
154+
branch,
155+
version,
156+
connectionsList,
157+
middlewareCounts,
158+
}) => {
159+
console.log(`Comparing branch ${branch} with Express version ${version}`)
160+
await runTests({
161+
identifier: branch,
162+
connectionsList,
163+
middlewareCounts,
164+
outputFile: `benchmarks/results_${branch}.md`,
165+
isVersionTest: false
166+
})
167+
await runTests({
168+
identifier: version,
169+
connectionsList,
170+
middlewareCounts,
171+
outputFile: `benchmarks/results_${version}.md`,
172+
isVersionTest: true
173+
})
174+
}
175+
176+
const main = async () => {
177+
const connectionsList = [50, 100, 250]
178+
const middlewareCounts = [1, 10, 25, 50]
179+
const prevBranch = process.env.PREV_BRANCH
180+
const currBranch = process.env.CURR_BRANCH
181+
const prevVersion = process.env.PREV_VERSION
182+
const currVersion = process.env.CURR_VERSION
183+
const version = process.env.VERSION
184+
const branch = process.env.BRANCH
185+
186+
if (prevBranch && currBranch) {
187+
await compareBranches({
188+
prevBranch,
189+
currBranch,
190+
connectionsList,
191+
middlewareCounts,
192+
})
193+
return
194+
}
195+
196+
if (prevVersion && currVersion) {
197+
await compareVersions({
198+
prevVersion,
199+
currVersion,
200+
connectionsList,
201+
middlewareCounts,
202+
})
203+
return
204+
}
205+
206+
if (branch && version) {
207+
await compareBranchAndVersion({
208+
branch,
209+
version,
210+
connectionsList,
211+
middlewareCounts,
212+
})
213+
return
214+
}
215+
216+
console.error(
217+
'Invalid input combination. Provide either two branches, two versions, or one branch and one version.'
218+
)
219+
process.exit(1)
220+
}
221+
222+
main()

benchmarks/middleware.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11

2-
var express = require('..');
3-
var app = express();
2+
const express = process.env.NO_LOCAL_EXPRESS === "true" ? require('express') : require('..');
3+
const app = express();
44

55
// number of middleware
66

7-
var n = parseInt(process.env.MW || '1', 10);
7+
let n = parseInt(process.env.MW || '1', 10);
88
console.log(' %s middleware', n);
99

1010
while (n--) {

0 commit comments

Comments
 (0)