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

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
bench:
name: Benchmark & Regression Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-bench-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-bench-

- name: Install cargo-criterion
run: cargo install cargo-criterion --version 1.1.0 --locked

- name: Run benchmarks
run: cargo criterion --output-format bencher 2>&1 | tee benchmark-results.txt
working-directory: contracts/payment-processing-contract
continue-on-error: true

- name: Store benchmark results
uses: benchmark-action/github-action-benchmark@v1
with:
tool: cargo
output-file-path: contracts/payment-processing-contract/benchmark-results.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: ${{ github.ref == 'refs/heads/main' }}
alert-threshold: '120%'
comment-on-alert: true
fail-on-alert: true
alert-comment-cc-users: '@ocheeluma'

wasm-size:
name: WASM Bundle Size
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-wasm-size-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-wasm-size-

- name: Build WASM (release)
run: cargo build --target wasm32-unknown-unknown --release --locked
working-directory: contracts/payment-processing-contract

- name: Analyse WASM size
run: |
WASM=target/wasm32-unknown-unknown/release/payment_processing_contract.wasm
SIZE=$(wc -c < "$WASM")
SIZE_KB=$(( SIZE / 1024 ))
echo "## WASM Bundle Size" >> "$GITHUB_STEP_SUMMARY"
echo "| Metric | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Binary size | ${SIZE_KB} KB (${SIZE} bytes) |" >> "$GITHUB_STEP_SUMMARY"
echo "wasm_size_bytes=${SIZE}" >> "$GITHUB_ENV"
if [ "$SIZE" -gt 102400 ]; then
echo "::error::WASM binary exceeds 100 KB limit (${SIZE_KB} KB)."
exit 1
elif [ "$SIZE" -gt 81920 ]; then
echo "::warning::WASM binary above 80 KB warning threshold (${SIZE_KB} KB)."
fi

- name: Download previous size baseline
uses: actions/cache@v4
with:
path: .wasm-size-baseline
key: wasm-size-baseline-${{ github.base_ref || github.ref_name }}

- name: Check size regression
run: |
CURRENT=$wasm_size_bytes
if [ -f .wasm-size-baseline ]; then
PREV=$(cat .wasm-size-baseline)
DELTA=$(( CURRENT - PREV ))
PCT=$(echo "scale=1; $DELTA * 100 / $PREV" | bc)
echo "Previous: ${PREV} bytes | Current: ${CURRENT} bytes | Delta: ${DELTA} bytes (${PCT}%)"
if [ "$DELTA" -gt 5120 ]; then
echo "::error::WASM size increased by ${DELTA} bytes (${PCT}%). Investigate before merging."
exit 1
fi
fi
echo "$CURRENT" > .wasm-size-baseline

api-perf:
name: API Response Time
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: api/package.json

- name: Install dependencies
run: npm ci
working-directory: api

- name: Run API performance tests
run: node scripts/perf-test.js
working-directory: api
env:
PERF_THRESHOLD_MS: '200'

compile-time:
name: Compile Time Profiling
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-timings-${{ hashFiles('**/Cargo.lock') }}

- name: Build with timings
run: cargo build --timings --locked 2>&1
working-directory: contracts/payment-processing-contract

- name: Upload timings report
uses: actions/upload-artifact@v4
with:
name: cargo-timings
path: contracts/payment-processing-contract/target/cargo-timings/
retention-days: 14

perf-summary:
name: Performance Summary
runs-on: ubuntu-latest
needs: [bench, wasm-size, api-perf, compile-time]
if: always()
steps:
- name: Report
run: |
echo "## Performance Check Results" >> "$GITHUB_STEP_SUMMARY"
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Benchmarks | ${{ needs.bench.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| WASM Size | ${{ needs.wasm-size.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| API Response Time | ${{ needs.api-perf.result }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| Compile Time | ${{ needs.compile-time.result }} |" >> "$GITHUB_STEP_SUMMARY"
if [[ "${{ needs.bench.result }}" == "failure" || \
"${{ needs.wasm-size.result }}" == "failure" || \
"${{ needs.api-perf.result }}" == "failure" ]]; then
echo "::error::Performance regression detected. Review the job logs above."
exit 1
fi
69 changes: 69 additions & 0 deletions api/scripts/perf-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node
// Lightweight API response-time check — runs against a local server stub.
// Fails if any endpoint median exceeds PERF_THRESHOLD_MS (default 200 ms).

'use strict';

const http = require('http');

const THRESHOLD = parseInt(process.env.PERF_THRESHOLD_MS || '200', 10);
const ITERATIONS = 20;

const endpoints = [
{ method: 'GET', path: '/health' },
{ method: 'GET', path: '/api/payments?limit=10' },
{ method: 'GET', path: '/api/merchants?limit=10' },
];

function measure(opts) {
return new Promise((resolve, reject) => {
const start = Date.now();
const req = http.request(opts, (res) => {
res.resume();
res.on('end', () => resolve(Date.now() - start));
});
req.on('error', reject);
req.setTimeout(5000, () => { req.destroy(); reject(new Error('timeout')); });
req.end();
});
}

async function bench(endpoint) {
const times = [];
for (let i = 0; i < ITERATIONS; i++) {
try {
times.push(await measure({ host: 'localhost', port: 3000, ...endpoint }));
} catch {
// Server not running in CI — skip gracefully
return null;
}
}
times.sort((a, b) => a - b);
return {
median: times[Math.floor(times.length / 2)],
p95: times[Math.floor(times.length * 0.95)],
min: times[0],
max: times[times.length - 1],
};
}

(async () => {
let failed = false;
console.log(`Performance threshold: ${THRESHOLD} ms\n`);

for (const ep of endpoints) {
const result = await bench(ep);
if (!result) {
console.log(`${ep.method} ${ep.path}: skipped (server not available)`);
continue;
}
const status = result.median > THRESHOLD ? 'FAIL' : 'PASS';
if (status === 'FAIL') failed = true;
console.log(
`[${status}] ${ep.method} ${ep.path} — ` +
`median=${result.median}ms p95=${result.p95}ms min=${result.min}ms max=${result.max}ms`
);
}

process.exit(failed ? 1 : 0);
})();
39 changes: 39 additions & 0 deletions docs/performance-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Performance Optimization Pipeline

Automated performance testing runs on every push and pull request via `.github/workflows/performance.yml`.

## Jobs

| Job | Tool | Blocks PR |
|-----|------|-----------|
| Benchmark & Regression | cargo-criterion + github-action-benchmark | Yes (>120% regression) |
| WASM Bundle Size | wc / baseline diff | Yes (>100 KB or >5 KB increase) |
| API Response Time | custom Node script | Yes (median >200 ms) |
| Compile Time Profiling | `cargo build --timings` | No (artifact only) |

## Regression Detection

- Benchmark results are stored via `github-action-benchmark`. On `main` pushes the baseline is updated; on PRs the action compares against the stored baseline and comments if any benchmark regresses beyond 120%.
- WASM size is cached per branch. A size increase of more than 5 KB triggers a build failure.

## Thresholds

| Metric | Warning | Failure |
|--------|---------|---------|
| WASM size | > 80 KB | > 100 KB or +5 KB vs baseline |
| Benchmark | — | > 120% of baseline |
| API median response | — | > 200 ms (configurable via `PERF_THRESHOLD_MS`) |

## Artifacts

- `cargo-timings` HTML report uploaded for 14 days per run (useful for identifying slow-compiling crates).

## Local Benchmark Run

```bash
cd contracts/payment-processing-contract
cargo install cargo-criterion --locked
cargo criterion
```

Results appear in `target/criterion/`.