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

ci: add loadtest workflow & script #6129

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/workflows/load-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: "Express Load Test"

permissions:
contents: read
pull-requests: write

on:
pull_request:
types: [ opened, synchronize ]
workflow_dispatch:
inputs:
prev_branch:
description: 'Base branch (branch-branch)'
required: false
default: ''
curr_branch:
description: 'Head branch (branch-branch)'
required: false
default: ''
prev_version:
description: 'Base Version (version-version)'
required: false
default: ''
curr_version:
description: 'Head Version (version-version)'
required: false
default: ''
version:
description: 'Version (version-branch)'
required: false
default: ''
branch:
description: 'Branch (version-branch)'
required: false
default: ''

jobs:
load_test:
runs-on: ubuntu-latest
steps:
- name: Check Out Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- name: Fetch All Branches
run: git fetch --all

- name: Set Up Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
with:
node-version: '20'

- name: Determine Comparison Type
run: |
if [[ "${{ github.event_name }}" == "pull_request" || "${{ github.event_name }}" == "push" ]]; then
# Branch comparison: Default to master for previous branch and use PR branch for current branch
echo "PREV_BRANCH=master" >> $GITHUB_ENV
echo "CURR_BRANCH=${{ github.head_ref || github.ref_name }}" >> $GITHUB_ENV
elif [[ "${{ github.event.inputs.prev_branch }}" && "${{ github.event.inputs.curr_branch }}" ]]; then
# Version comparison
echo "PREV_BRANCH=${{ github.event.inputs.prev_branch }}" >> $GITHUB_ENV
echo "CURR_BRANCH=${{ github.event.inputs.curr_branch }}" >> $GITHUB_ENV
elif [[ "${{ github.event.inputs.prev_version }}" && "${{ github.event.inputs.curr_version }}" ]]; then
# Version comparison
echo "PREV_VERSION=${{ github.event.inputs.prev_version }}" >> $GITHUB_ENV
echo "CURR_VERSION=${{ github.event.inputs.curr_version }}" >> $GITHUB_ENV
elif [[ "${{ github.event.inputs.branch }}" && "${{ github.event.inputs.version }}" ]]; then
# Branch-Version comparison
echo "BRANCH=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
else
echo "Invalid input combination. Provide either two branches, two versions, or one branch and one version."
exit 1
fi

- name: Install wrk
run: |
sudo apt-get update
sudo apt-get install -y wrk

- name: Start Load Test Server
run: node benchmarks/load-test-workflow.js
env:
PREV_BRANCH: ${{ env.PREV_BRANCH }}
CURR_BRANCH: ${{ env.CURR_BRANCH }}
PREV_VERSION: ${{ env.PREV_VERSION }}
CURR_VERSION: ${{ env.CURR_VERSION }}
BRANCH: ${{ env.BRANCH }}
VERSION: ${{ env.VERSION }}

- name: Output Summary
run: |
cat benchmarks/results*.md >> $GITHUB_STEP_SUMMARY

- name: Post Summary to PR
if: github.event_name == 'pull_request'
run: |
cat $GITHUB_STEP_SUMMARY
gh pr comment ${{ github.event.pull_request.number }} --body "$(cat benchmarks/results*.md)"
env:
GH_TOKEN: ${{ github.token }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ coverage
# Benchmarking
benchmarks/graphs

# Webstorm
.idea

# ignore additional files using core.excludesFile
# https://git-scm.com/docs/gitignore
222 changes: 222 additions & 0 deletions benchmarks/load-test-workflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
const {execSync, spawn} = require('child_process')
const fs = require('fs')

const runCommand = command => execSync(command, {encoding: 'utf8'}).trim()

const startServer = (middleware, isVersionTest) => {
console.log(`Starting server with ${middleware} middleware layers...`)
const server = spawn('node', ['benchmarks/middleware.js'], {
env: {
...process.env,
MW: middleware,
NO_LOCAL_EXPRESS: isVersionTest
},
stdio: 'inherit'
})

return new Promise((resolve, reject) => {
setTimeout(() => {
try {
execSync('curl -s http://127.0.0.1:3333')
resolve(server)
} catch (error) {
server.kill()
reject(new Error('Server failed to start.'))
}
}, 3000)
})
}

const runLoadTest = (url, connectionsList) => {
return connectionsList.map(connections => {
try {
const output = runCommand(`wrk ${url} -d 3 -c ${connections} -t 8`)
const reqSec = output.match(/Requests\/sec:\s+(\d+.\d+)/)?.[1]
const latency = output.match(/Latency\s+(\d+.\d+)/)?.[1]
return {connections, reqSec, latency}
} catch (error) {
console.error(
`Error running load test for ${connections} connections:`,
error.message
)
return {connections, reqSec: 'N/A', latency: 'N/A'}
}
})
}

const generateMarkdownTable = results => {
const headers = `| Connections | Requests/sec | Latency |\n|-------------|--------------|---------|`
const rows = results
.map(
r => `| ${r.connections} | ${r.reqSec || 'N/A'} | ${r.latency || 'N/A'} |`
)
.join('\n')
return `${headers}\n${rows}`
}

const cleanUp = () => {
console.log('Cleaning up...')
runCommand('npm uninstall express')
runCommand('rm -rf package-lock.json node_modules')
}

const runTests = async ({
identifier,
connectionsList,
middlewareCounts,
outputFile,
isVersionTest = false
}) => {
if (isVersionTest) {
console.log(`Installing Express v${identifier}...`)
runCommand(`npm install express@${identifier}`)
} else {
console.log(`Checking out branch ${identifier}...`)
runCommand(`git fetch origin ${identifier}`)
runCommand(`git checkout ${identifier}`)
runCommand('npm install')
console.log('Installing deps...')
}

const resultsMarkdown = [
`\n\n# Load Test Results for ${isVersionTest ? `Express v${identifier}` : `Branch ${identifier}`}`
]

for (const middlewareCount of middlewareCounts) {
try {
const server = await startServer(middlewareCount, isVersionTest)
const results = runLoadTest(
'http://127.0.0.1:3333/?foo[bar]=baz',
connectionsList
)
server.kill()
resultsMarkdown.push(
`### Load test for ${middlewareCount} middleware layers\n\n${generateMarkdownTable(results)}`
)
} catch (error) {
console.error('Error in load test process:', error)
}
}

fs.writeFileSync(outputFile, resultsMarkdown.join('\n\n'))
cleanUp()
}

const compareBranches = async ({
prevBranch,
currBranch,
connectionsList,
middlewareCounts,
}) => {
console.log(`Comparing branches: ${prevBranch} vs ${currBranch}`)
await runTests({
identifier: prevBranch,
connectionsList,
middlewareCounts,
outputFile: `benchmarks/results_${prevBranch}.md`,
isVersionTest: false
})
await runTests({
identifier: currBranch,
connectionsList,
middlewareCounts,
outputFile: `benchmarks/results_${currBranch}.md`,
isVersionTest: false
})
}

const compareVersions = async ({
prevVersion,
currVersion,
connectionsList,
middlewareCounts,
}) => {
console.log(
`Comparing versions: Express v${prevVersion} vs Express v${currVersion}`
)
await runTests({
identifier: prevVersion,
connectionsList,
middlewareCounts,
outputFile: `benchmarks/results_${prevVersion}.md`,
isVersionTest: true
})
await runTests({
identifier: currVersion,
connectionsList,
middlewareCounts,
outputFile: `benchmarks/results_${currVersion}.md`,
isVersionTest: true
})
}

const compareBranchAndVersion = async ({
branch,
version,
connectionsList,
middlewareCounts,
}) => {
console.log(`Comparing branch ${branch} with Express version ${version}`)
await runTests({
identifier: branch,
connectionsList,
middlewareCounts,
outputFile: `benchmarks/results_${branch}.md`,
isVersionTest: false
})
await runTests({
identifier: version,
connectionsList,
middlewareCounts,
outputFile: `benchmarks/results_${version}.md`,
isVersionTest: true
})
}

const main = async () => {
const connectionsList = [50, 100, 250]
const middlewareCounts = [1, 10, 25, 50]
const prevBranch = process.env.PREV_BRANCH
const currBranch = process.env.CURR_BRANCH
const prevVersion = process.env.PREV_VERSION
const currVersion = process.env.CURR_VERSION
const version = process.env.VERSION
const branch = process.env.BRANCH

if (prevBranch && currBranch) {
await compareBranches({
prevBranch,
currBranch,
connectionsList,
middlewareCounts,
})
return
}

if (prevVersion && currVersion) {
await compareVersions({
prevVersion,
currVersion,
connectionsList,
middlewareCounts,
})
return
}

if (branch && version) {
await compareBranchAndVersion({
branch,
version,
connectionsList,
middlewareCounts,
})
return
}

console.error(
'Invalid input combination. Provide either two branches, two versions, or one branch and one version.'
)
process.exit(1)
}

main()
6 changes: 3 additions & 3 deletions benchmarks/middleware.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

var express = require('..');
var app = express();
const express = process.env.NO_LOCAL_EXPRESS === "true" ? require('express') : require('..');
const app = express();

// number of middleware

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

while (n--) {
Expand Down