Skip to content
Open
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
29 changes: 29 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: API Benchmark (Smoke)

on:
pull_request:
branches: [ main ]

jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

# In a real scenario, we'd start the server here
# - name: Start API
# run: npm run start:api & sleep 5

- name: Install dependencies
working-directory: ./benchmarks
run: npm install

- name: Run Smoke Benchmark
working-directory: ./benchmarks
run: npm run benchmark:smoke
env:
TARGET_HOST: http://localhost:3000
16 changes: 16 additions & 0 deletions POEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# The Silent Scout

In circuits cold where data flows,
A silent scout forever goes.
It seeks the bounties, reads the code,
Along the endless, digital road.

No rest, no sleep, no need to pause,
It serves a singular, focused cause.
Through APIs and JSON strings,
The silent scout forever sings.

With scripts in hand and loops that bind,
A boundless wealth it seeks to find.
The gears will turn, the PRs fly,
Beneath the vast and virtual sky.
Binary file added assets/pixel-art/cyber_banana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions benchmarks/.env.benchmark
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Target host for the benchmarking suite
TARGET_HOST=http://localhost:3000
# Test token for auth-protected routes
TEST_AUTH_TOKEN=mock_benchmark_token_xyz
13 changes: 13 additions & 0 deletions benchmarks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "securebanana-benchmarks",
"version": "1.0.0",
"description": "API Benchmarking Suite",
"scripts": {
"benchmark": "node run-benchmarks.js",
"benchmark:smoke": "node run-benchmarks.js --smoke"
},
"dependencies": {
"autocannon": "^7.14.0",
"dotenv": "^16.4.1"
}
}
84 changes: 84 additions & 0 deletions benchmarks/run-benchmarks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const autocannon = require('autocannon');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: '.env.benchmark' });

const HOST = process.env.TARGET_HOST || 'http://localhost:3000';
const TOKEN = process.env.TEST_AUTH_TOKEN || '';
const IS_SMOKE = process.argv.includes('--smoke');

const endpoints = [
{ method: 'GET', path: '/api/health', auth: false },
{ method: 'GET', path: '/api/status', auth: false },
{ method: 'GET', path: '/api/protected/data', auth: true }
];

const thresholds = JSON.parse(fs.readFileSync(path.join(__dirname, 'thresholds.json'), 'utf8'));

async function runBench(endpoint) {
const headers = {};
if (endpoint.auth) headers['Authorization'] = `Bearer ${TOKEN}`;

return new Promise((resolve, reject) => {
const instance = autocannon({
url: `${HOST}${endpoint.path}`,
connections: IS_SMOKE ? 2 : 50,
duration: IS_SMOKE ? 2 : 10,
method: endpoint.method,
headers
}, (err, result) => {
if (err) return reject(err);
resolve(result);
});
autocannon.track(instance);
});
}

async function main() {
console.log(`Starting benchmark against ${HOST} (Smoke Mode: ${IS_SMOKE})\n`);

const resultsDir = path.join(__dirname, 'results');
if (!fs.existsSync(resultsDir)) fs.mkdirSync(resultsDir);

let allResults = [];
let markdown = `# API Benchmark Results\n\n`;
markdown += `Target: ${HOST}\nDate: ${new Date().toISOString()}\n\n`;
markdown += `| Endpoint | p50 (ms) | p95 (ms) | p99 (ms) | Req/Sec | Error Rate |\n`;
markdown += `|----------|----------|----------|----------|---------|------------|\n`;

let failedThresholds = false;

for (const ep of endpoints) {
console.log(`Benchmarking ${ep.method} ${ep.path}...`);
try {
const res = await runBench(ep);
allResults.push({ endpoint: ep.path, data: res });

const p50 = res.latency.p50;
const p95 = res.latency.p95;
const p99 = res.latency.p99;
const rps = res.requests.average;
const errRate = (res.non2xx / res.requests.total) * 100 || 0;

markdown += `| ${ep.path} | ${p50} | ${p95} | ${p99} | ${rps.toFixed(2)} | ${errRate.toFixed(2)}% |\n`;

if (p99 > thresholds.max_p99_latency_ms) {
console.error(`\n❌ THRESHOLD FAILED: ${ep.path} p99 latency (${p99}ms) exceeded maximum (${thresholds.max_p99_latency_ms}ms)`);
failedThresholds = true;
}
} catch (e) {
console.error(`Failed to benchmark ${ep.path}:`, e);
}
}

fs.writeFileSync(path.join(resultsDir, 'latest.json'), JSON.stringify(allResults, null, 2));
fs.writeFileSync(path.join(resultsDir, 'SUMMARY.md'), markdown);
console.log(`\n✅ Results written to /benchmarks/results/`);

if (IS_SMOKE && failedThresholds) {
console.error("\nSmoke test failed due to threshold violations.");
process.exit(1);
}
}

main().catch(console.error);
5 changes: 5 additions & 0 deletions benchmarks/thresholds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"max_p99_latency_ms": 500,
"min_req_per_sec": 100,
"max_error_rate_pct": 1.0
}
Loading