diff --git a/.github/workflows/load-tests.yml b/.github/workflows/load-tests.yml new file mode 100644 index 000000000..4bda65fd4 --- /dev/null +++ b/.github/workflows/load-tests.yml @@ -0,0 +1,186 @@ +name: Load Tests + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + +jobs: + test-base: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.base.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Initialize the project + run: npm run init + + - name: Build + run: npm run build + + - name: Copy devnet config file from src to dist + run: cp ./config/config.devnet.yaml ./dist/config/config.yaml + + - name: Start docker services + run: docker compose up -d + + - name: Start Node.js API + run: node ./dist/src/main.js & + + - name: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update + sudo apt-get install k6 + + - name: Wait for API to be ready + run: | + until curl --output /dev/null --silent --fail http://localhost:4001/hello; do + echo 'Waiting for API...' + sleep 1 + done + + - name: Preload cache + run: k6 run ./k6/preload.js + + - name: Run k6 Load Test + run: k6 run ./k6/script.js + + - name: Upload result file for base branch + uses: actions/upload-artifact@v3 + with: + name: base-results + path: k6/output/summary.json + + - name: Stop docker services + run: docker compose down + + test-head: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Initialize the project + run: npm run init + + - name: Build + run: npm run build + + - name: Copy devnet config file from src to dist + run: cp ./config/config.devnet.yaml ./dist/config/config.yaml + + - name: Start docker services + run: docker compose up -d + + - name: Start Node.js API + run: node ./dist/src/main.js & + + - name: Install k6 + run: | + sudo gpg -k + sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 + echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update + sudo apt-get install k6 + + - name: Wait for API to be ready + run: | + until curl --output /dev/null --silent --fail http://localhost:4001/hello; do + echo 'Waiting for API...' + sleep 1 + done + + - name: Preload cache + run: k6 run ./k6/preload.js + + - name: Run k6 Load Test + run: k6 run ./k6/script.js + + - name: Upload result file for head branch + uses: actions/upload-artifact@v3 + with: + name: head-results + path: k6/output/summary.json + + - name: Stop docker services + run: docker compose down + + compare-results: + runs-on: ubuntu-latest + + needs: [test-base, test-head] + steps: + - uses: actions/checkout@v2 + + - name: Download all artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '20' + + - name: Compare test results + run: | + node ./k6/compare-results.js ${{ github.event.pull_request.base.sha }} artifacts/base-results/summary.json ${{ github.event.pull_request.head.sha }} artifacts/head-results/summary.json report.md + + - name: Render the report from the template + id: template + uses: chuhlomin/render-template@v1 + if: github.event_name == 'pull_request' + with: + template: report.md + vars: | + base: ${{ github.event.pull_request.base.sha }} + head: ${{ github.event.pull_request.head.sha }} + + - name: Upload the report markdown + uses: actions/upload-artifact@v3 + if: github.event_name == 'pull_request' + with: + name: report-markdown + path: report.md + + - name: Find the comment containing the report + id: fc + uses: peter-evans/find-comment@v2 + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: 'k6 load testing comparison' + + - name: Create or update the report comment + uses: peter-evans/create-or-update-comment@v2 + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: ${{ steps.template.outputs.result }} + edit-mode: replace diff --git a/k6/.gitignore b/k6/.gitignore new file mode 100644 index 000000000..77d1aa0a2 --- /dev/null +++ b/k6/.gitignore @@ -0,0 +1,2 @@ +output/* +!output/.gitkeep \ No newline at end of file diff --git a/k6/compare-results.js b/k6/compare-results.js new file mode 100644 index 000000000..c7be7661c --- /dev/null +++ b/k6/compare-results.js @@ -0,0 +1,124 @@ +const fs = require('fs'); + +function generateComparisonTable(baseCommitHash, baseMetricsPath, targetCommitHash, targetMetricsPath, outputPath) { + // Load JSON outputs from k6 + const baseMetrics = JSON.parse(fs.readFileSync(baseMetricsPath, 'utf8')); + const targetMetrics = JSON.parse(fs.readFileSync(targetMetricsPath, 'utf8')); + + const baseData = extractMetrics(baseMetrics); + const targetData = extractMetrics(targetMetrics); + + const table = generateTable(baseCommitHash, baseData, targetCommitHash, targetData); + + fs.writeFileSync(outputPath, table); +} + +function extractMetrics(metrics) { + const extractedMetrics = {}; + const metricKeys = Object.keys(metrics.metrics); + + for (const key of metricKeys) { + if (key.endsWith('_http_req_duration')) { + const values = metrics.metrics[key].values; + const avgResponseTime = values.avg; + const maxResponseTime = values.max; + const p90 = values['p(90)']; + const p95 = values['p(95)']; + + const name = key.split('_')[0].charAt(0).toUpperCase() + key.split('_')[0].slice(1); + + if (!extractedMetrics[name]) { + extractedMetrics[name] = { avgResponseTime, maxResponseTime, p90, p95 }; + } else { + extractedMetrics[name].avgResponseTime = avgResponseTime; + extractedMetrics[name].maxResponseTime = maxResponseTime; + extractedMetrics[name].p90 = p90; + extractedMetrics[name].p95 = p95; + } + } + } + + extractedMetrics['Test Run Duration'] = metrics.state.testRunDurationMs; + + return extractedMetrics; +} + +function generateTable(baseCommitHash, baseData, targetCommitHash, targetData) { + const headers = ['Avg', 'Max', '90', '95']; + let table = `k6 load testing comparison.\nBase Commit Hash: ${baseCommitHash}\nTarget Commit Hash: ${targetCommitHash}\n\n`; + table += '
Metric | \ +Base | \ +Target | \ +Diff | \ +|||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
${header} | `; + }); + } + table += '||||||||||||
${key} | `; + table += `${baseAvg.toFixed(2)} | ${baseMax.toFixed(2)} | ${baseP90.toFixed(2)} | ${baseP95.toFixed(2)} | `; + table += `${targetAvg.toFixed(2)} | ${targetMax.toFixed(2)} | ${targetP90.toFixed(2)} | ${targetP95.toFixed(2)} | `; + table += `${avgDiff} ${avgColor} | ${maxDiff} ${maxColor} | ${p90Diff} ${p90Color} | ${p95Diff} ${p95Color} |
Test Run Duration | ${baseDuration} | ${targetDuration} |