Skip to content
Closed
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
73 changes: 73 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Performance Benchmarks

on:
push:
branches: [main]
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
workflow_dispatch:
inputs:
iterations:
description: 'Number of benchmark iterations'
required: false
default: '3'
type: string

permissions:
contents: read

jobs:
benchmark:
name: Performance Benchmarks
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build project
run: npm run build

- name: Pre-benchmark cleanup
run: sudo ./scripts/ci/cleanup.sh

- name: Run performance benchmarks
id: run-benchmarks
run: |
sudo -E npm run test:benchmark 2>&1 | tee benchmark-output.log
continue-on-error: true

- name: Generate benchmark summary
if: always()
run: |
npx tsx scripts/ci/generate-benchmark-summary.ts benchmark-output.log

- name: Check benchmark results
if: steps.run-benchmarks.outcome == 'failure'
run: exit 1

- name: Post-benchmark cleanup
if: always()
run: sudo ./scripts/ci/cleanup.sh

- name: Upload benchmark report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: benchmark-report
path: |
/tmp/awf-benchmark-report.json
benchmark-output.log
retention-days: 30
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"test": "jest",
"test:unit": "jest --config jest.config.js",
"test:integration": "jest --config tests/setup/jest.integration.config.js",
"test:benchmark": "jest --config tests/setup/jest.benchmark.config.js",
"test:all": "npm run test:unit && npm run test:integration",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
Expand Down
183 changes: 183 additions & 0 deletions scripts/ci/generate-benchmark-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env node
/**
* Generate GitHub Actions job summary from benchmark test output
* This script parses benchmark output and creates a markdown summary
* with performance metrics and statistics.
*/

import * as fs from 'fs';
import * as path from 'path';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import path.

Copilot Autofix

AI 4 months ago

In general, unused imports should be removed to make the code clearer and avoid confusion about their purpose. For this file, the simplest, behavior‑preserving fix is to delete the unused path import line.

Concretely, in scripts/ci/generate-benchmark-summary.ts, remove line 9: import * as path from 'path';. No other code changes are needed because nothing references path. No additional methods, definitions, or imports are required.

Suggested changeset 1
scripts/ci/generate-benchmark-summary.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/ci/generate-benchmark-summary.ts b/scripts/ci/generate-benchmark-summary.ts
--- a/scripts/ci/generate-benchmark-summary.ts
+++ b/scripts/ci/generate-benchmark-summary.ts
@@ -6,7 +6,6 @@
  */
 
 import * as fs from 'fs';
-import * as path from 'path';
 
 interface BenchmarkMetric {
   name: string;
EOF
@@ -6,7 +6,6 @@
*/

import * as fs from 'fs';
import * as path from 'path';

interface BenchmarkMetric {
name: string;
Copilot is powered by AI and may make mistakes. Always verify output.

interface BenchmarkMetric {
name: string;
metric: string;
unit: string;
values: number[];
}

interface ParsedResults {
metrics: BenchmarkMetric[];
passed: number;
failed: number;
duration: string;
}

function parseJestOutput(output: string): ParsedResults {
const lines = output.split('\n');
const metrics: BenchmarkMetric[] = [];
let passed = 0;
let failed = 0;
let duration = 'unknown';

// Parse test results
const testsLine = lines.find(line => line.startsWith('Tests:'));
if (testsLine) {
const passedMatch = testsLine.match(/(\d+) passed/);
const failedMatch = testsLine.match(/(\d+) failed/);
passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
}

// Parse duration
const timeLine = lines.find(line => line.match(/Time:\s+[\d.]+\s*s/));
if (timeLine) {
const timeMatch = timeLine.match(/Time:\s+([\d.]+\s*s)/);
if (timeMatch) {
duration = timeMatch[1];
}
}

// Parse benchmark-specific output
// Look for lines like: "container_startup: Iteration 1 completed - 5234 ms"
const benchmarkPattern = /\[Benchmark\] (\w+): Iteration \d+ completed - ([\d.]+) (\w+)/g;
let match;
while ((match = benchmarkPattern.exec(output)) !== null) {
const [, name, value, unit] = match;
const existingMetric = metrics.find(m => m.name === name);
if (existingMetric) {
existingMetric.values.push(parseFloat(value));
} else {
metrics.push({
name,
metric: name,
unit,
values: [parseFloat(value)],
});
}
}

return { metrics, passed, failed, duration };
}

function calculateStats(values: number[]): { min: number; max: number; mean: number; stdDev: number } {
if (values.length === 0) {
return { min: 0, max: 0, mean: 0, stdDev: 0 };
}

const sorted = [...values].sort((a, b) => a - b);
const sum = sorted.reduce((a, b) => a + b, 0);
const mean = sum / sorted.length;

const squaredDiffs = sorted.map(v => Math.pow(v - mean, 2));
const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / sorted.length;
const stdDev = Math.sqrt(avgSquaredDiff);

return {
min: sorted[0],
max: sorted[sorted.length - 1],
mean,
stdDev,
};
}

function generateSummary(output: string): string {
const results = parseJestOutput(output);
const statusEmoji = results.failed === 0 ? '✅' : '❌';

let summary = `## ${statusEmoji} Performance Benchmark Results\n\n`;
summary += `**Results:** ${results.passed} passed, ${results.failed} failed in ${results.duration}\n\n`;

if (results.metrics.length > 0) {
summary += '### Benchmark Metrics\n\n';
summary += '| Metric | Mean | Min | Max | Std Dev | Samples |\n';
summary += '|--------|------|-----|-----|---------|--------|\n';

for (const metric of results.metrics) {
const stats = calculateStats(metric.values);
summary += `| ${metric.name} | ${stats.mean.toFixed(2)} ${metric.unit} | `;
summary += `${stats.min.toFixed(2)} ${metric.unit} | `;
summary += `${stats.max.toFixed(2)} ${metric.unit} | `;
summary += `${stats.stdDev.toFixed(2)} | `;
summary += `${metric.values.length} |\n`;
}

summary += '\n';
}

// Add interpretation section
summary += '### Metric Descriptions\n\n';
summary += '| Metric | Description |\n';
summary += '|--------|-------------|\n';
summary += '| startup_time_ms | Time to start containers and execute a simple command |\n';
summary += '| request_time_ms | Time to make an HTTP request through the proxy |\n';
summary += '| download_time_ms | Time to download a small file through the proxy |\n';
summary += '| memory_mb | Combined memory usage of containers |\n';
summary += '| reject_time_ms | Time for proxy to reject a blocked domain request |\n';
summary += '\n';

// Try to load the full JSON report if available
const reportPath = '/tmp/awf-benchmark-report.json';
if (fs.existsSync(reportPath)) {
try {
const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
summary += '### Environment\n\n';
summary += `- **OS:** ${report.environment?.os || 'unknown'}\n`;
summary += `- **Node.js:** ${report.environment?.nodeVersion || 'unknown'}\n`;
summary += `- **CPU:** ${report.environment?.cpuModel || 'unknown'}\n`;
summary += `- **CPU Cores:** ${report.environment?.cpuCount || 'unknown'}\n`;
summary += `- **Memory:** ${report.environment?.totalMemoryMb || 'unknown'} MB\n`;
summary += `- **Commit:** \`${(report.commitSha || 'unknown').substring(0, 7)}\`\n`;
summary += '\n';
} catch (error) {
// Ignore parse errors
}
}

return summary;
}

function main() {
const args = process.argv.slice(2);

if (args.length < 1) {
console.error('Usage: generate-benchmark-summary.ts <output-file>');
process.exit(1);
}

const outputFile = args[0];

// Read benchmark output from file
let benchmarkOutput: string;
if (fs.existsSync(outputFile)) {
benchmarkOutput = fs.readFileSync(outputFile, 'utf-8');
} else {
console.error(`Error: Output file not found: ${outputFile}`);
process.exit(1);
}

// Generate summary
const summary = generateSummary(benchmarkOutput);

// Write to GITHUB_STEP_SUMMARY or stdout
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
if (summaryPath) {
fs.appendFileSync(summaryPath, summary);
console.log('Benchmark summary generated successfully');
} else {
console.error('Warning: GITHUB_STEP_SUMMARY not set. Running outside GitHub Actions?');
console.log('\n--- Benchmark Summary ---');
console.log(summary);
}
}

main();
Loading
Loading