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
44 changes: 43 additions & 1 deletion benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,46 @@
# Soroban Gas Consumption Benchmark CLI Tool
# Benchmarks

This directory contains performance and load testing tools for the Mobile Money bridge.

## k6 Ingest Load Testing

The k6 suite benchmarks high-throughput callback ingestion services (`ingest-node` on `:3001`, `ingest-go` on `:3002`).

### Prerequisites

- [k6](https://k6.io/docs/getting-started/installation/) installed
- Ingest service running locally
- Redis on `:6379`

### Scenarios

| Script | Purpose |
| ------ | ------- |
| `k6-bench.js` | Baseline constant-arrival-rate throughput (1k/5k/10k RPS) |
| `scenarios/smoke.js` | Quick 5-VU sanity check before full runs |
| `scenarios/peak-day-spike.js` | 30-min realistic peak-day traffic curve |
| `scenarios/stress.js` | Breaking-point ramp beyond peak load |

### Usage

```bash
# Run the full baseline suite
./benchmarks/run-bench.sh

# Run individual scenarios
./benchmarks/run-bench.sh --scenario smoke
./benchmarks/run-bench.sh --scenario peak-day
./benchmarks/run-bench.sh --scenario stress

# Direct k6 invocation
k6 run -e TARGET_URL=http://localhost:3001 benchmarks/scenarios/smoke.js
```

Results are written to `benchmarks/results/` (JSON exports are gitignored).

---

## Soroban Gas Consumption Benchmark CLI Tool

Automates gas measurement of Soroban smart contract deployments and method invocations.
Outputs clean gas figures as formatted terminal tables, JSON, and Markdown reports.
Expand Down
130 changes: 2 additions & 128 deletions benchmarks/scenarios/peak-day-spike.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,48 +17,22 @@
*
* # Override thresholds to observe-only (no fail)
* k6 run -e TARGET_URL=http://localhost:3001 -e OBSERVE_ONLY=true benchmarks/scenarios/peak-day-spike.js
*
* Output:
* Console summary + benchmarks/results/peak-day-spike-<timestamp>.json
*/

import http from "k6/http";
import { check, sleep } from "k6";
import { check } from "k6";
import { Rate, Trend, Counter } from "k6/metrics";

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

const TARGET_URL = __ENV.TARGET_URL || "http://localhost:3001";
const TARGET_URL = __ENV.TARGET_URL || "http://localhost:3001";
const OBSERVE_ONLY = __ENV.OBSERVE_ONLY === "true";

// ---------------------------------------------------------------------------
// Custom metrics
// ---------------------------------------------------------------------------

const errorRate = new Rate("spike_error_rate");
const publishLatency = new Trend("spike_publish_latency_ms", true);
const timeoutCount = new Counter("spike_timeout_count");
const successCount = new Counter("spike_success_count");
const errorRate = new Rate("spike_error_rate");
const publishLatency = new Trend("spike_publish_latency_ms", true);
const timeoutCount = new Counter("spike_timeout_count");
const successCount = new Counter("spike_success_count");

// ---------------------------------------------------------------------------
// Providers & currencies — realistic diversity
// ---------------------------------------------------------------------------

const PROVIDERS = ["mtn", "airtel", "orange", "vodacom", "mpesa"];
const CURRENCIES = ["XAF", "KES", "NGN", "GHS", "TZS", "UGX", "ZMW"];
const REGIONS = ["CM", "KE", "NG", "GH", "TZ", "UG", "ZM"];
const CHANNELS = ["mobile", "ussd", "api", "pos"];
const STATUSES = [
{ status: "success", weight: 85 },
{ status: "pending", weight: 10 },
{ status: "failed", weight: 5 },
const REGIONS = ["CM", "KE", "NG", "GH", "TZ", "UG", "ZM"];
const CHANNELS = ["mobile", "ussd", "api", "pos"];
const STATUSES = [
Expand All @@ -67,10 +41,6 @@ const STATUSES = [
{ status: "failed", weight: 5 },
];

// ---------------------------------------------------------------------------
// k6 options — ramping-arrival-rate models real-world traffic curves
// ---------------------------------------------------------------------------

export const options = {
scenarios: {
peak_day_spike: {
Expand All @@ -80,32 +50,12 @@ export const options = {
preAllocatedVUs: 2000,
maxVUs: 40000,
stages: [
// Phase 1 — Baseline
{ target: 500, duration: "2m" },
// Phase 2 — Ramp-up
{ target: 3000, duration: "3m" },
// Phase 3 — Morning peak climb
{ target: 8000, duration: "5m" },
// Phase 4 — Sustained peak
{ target: 8000, duration: "10m" },
// Phase 5 — Flash spike
{ target: 15000, duration: "2m" },
// Phase 6 — Recovery
{ target: 2000, duration: "5m" },
// Phase 7 — Cool-down
{ target: 500, duration: "3m" },
{ target: 500, duration: "2m" },
// Phase 2 — Ramp-up
{ target: 3000, duration: "3m" },
// Phase 3 — Morning peak climb
{ target: 8000, duration: "5m" },
// Phase 4 — Sustained peak
{ target: 8000, duration: "10m" },
// Phase 5 — Flash spike
{ target: 15000, duration: "2m" },
// Phase 6 — Recovery
{ target: 2000, duration: "5m" },
// Phase 7 — Cool-down
{ target: 500, duration: "3m" },
],
},
Expand All @@ -114,22 +64,11 @@ export const options = {
thresholds: OBSERVE_ONLY
? {}
: {
// Latency must stay within acceptable bounds across the spike
http_req_duration: [
"p(50)<100", // P50 < 100 ms
"p(95)<500", // P95 < 500 ms
"p(99)<1000", // P99 < 1 s
"p(50)<100", // P50 < 100 ms
"p(95)<500", // P95 < 500 ms
"p(99)<1000", // P99 < 1 s
],
// Error budget: tolerate up to 2% during spike, 0.5% at baseline
http_req_duration: ["p(50)<100", "p(95)<500", "p(99)<1000"],
spike_error_rate: ["rate<0.02"],
// Timeouts should be rare even at peak
spike_timeout_count: ["count<500"],
},

summaryTrendStats: ["min", "med", "avg", "p(90)", "p(95)", "p(99)", "p(99.9)", "max", "count"],
summaryTrendStats: [
"min",
"med",
Expand All @@ -143,11 +82,6 @@ export const options = {
],
};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Weighted random pick from [{status, weight}] */
function weightedRandom(items) {
const total = items.reduce((sum, i) => sum + i.weight, 0);
let rand = Math.random() * total;
Expand All @@ -162,29 +96,15 @@ function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}

/** Build a realistic, varied payment callback payload */
function makePayload() {
const provider = pick(PROVIDERS);
const idx = PROVIDERS.indexOf(provider);
const currency = CURRENCIES[idx] || "XAF";
const region = REGIONS[idx] || "CM";
const idx = PROVIDERS.indexOf(provider);
const currency = CURRENCIES[idx] || "XAF";
const region = REGIONS[idx] || "CM";

return JSON.stringify({
event_type: "payment.callback",
provider,
reference: `REF-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
amount: parseFloat((Math.random() * 50000 + 100).toFixed(2)),
currency,
status: weightedRandom(STATUSES),
timestamp: new Date().toISOString(),
metadata: {
customer_id: `cust-${Math.random().toString(36).slice(2, 10)}`,
channel: pick(CHANNELS),
region,
session_id: `sess-${Date.now()}`,
reference: `REF-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
amount: parseFloat((Math.random() * 50000 + 100).toFixed(2)),
currency,
Expand All @@ -199,10 +119,6 @@ function makePayload() {
});
}

// ---------------------------------------------------------------------------
// Default function — executed once per VU iteration
// ---------------------------------------------------------------------------

export default function () {
const start = Date.now();

Expand All @@ -214,7 +130,6 @@ export default function () {
const latency = Date.now() - start;
publishLatency.add(latency);

// Track timeouts specifically (k6 returns status 0 on network errors)
if (res.status === 0) {
timeoutCount.add(1);
errorRate.add(1);
Expand All @@ -223,11 +138,6 @@ export default function () {

const ok = check(res, {
"status 202 (accepted)": (r) => r.status === 202,
"has reference field": (r) => {
try { return r.json("reference") !== undefined; }
catch { return false; }
},
"response time < 1s": (r) => r.timings.duration < 1000,
"has reference field": (r) => {
try {
return r.json("reference") !== undefined;
Expand All @@ -242,24 +152,7 @@ export default function () {
if (ok) successCount.add(1);
}

// ---------------------------------------------------------------------------
// Summary — rich console output + JSON export
// ---------------------------------------------------------------------------

export function handleSummary(data) {
const m = data.metrics;
const dur = m.http_req_duration?.values;
const rps = m.http_reqs?.values?.rate?.toFixed(1) ?? "N/A";
const p50 = dur?.["p(50)"]?.toFixed(2) ?? "N/A";
const p90 = dur?.["p(90)"]?.toFixed(2) ?? "N/A";
const p95 = dur?.["p(95)"]?.toFixed(2) ?? "N/A";
const p99 = dur?.["p(99)"]?.toFixed(2) ?? "N/A";
const p999 = dur?.["p(99.9)"]?.toFixed(2) ?? "N/A";
const maxL = dur?.max?.toFixed(2) ?? "N/A";
const totalReqs = m.http_reqs?.values?.count ?? 0;
const errRate = ((m.spike_error_rate?.values?.rate ?? 0) * 100).toFixed(2);
const timeouts = m.spike_timeout_count?.values?.count ?? 0;
const successes = m.spike_success_count?.values?.count ?? 0;
const m = data.metrics;
const dur = m.http_req_duration?.values;
const rps = m.http_reqs?.values?.rate?.toFixed(1) ?? "N/A";
Expand Down Expand Up @@ -288,32 +181,13 @@ export function handleSummary(data) {
console.log(`║ P99 : ${p99.padEnd(8)} ms ║`);
console.log(`║ P99.9 : ${p999.padEnd(8)} ms ║`);
console.log(`║ Max : ${maxL.padEnd(8)} ms ║`);
console.log(
`║ P50 : ${p50.padEnd(8)} ms ║`,
);
console.log(
`║ P90 : ${p90.padEnd(8)} ms ║`,
);
console.log(
`║ P95 : ${p95.padEnd(8)} ms ║`,
);
console.log(
`║ P99 : ${p99.padEnd(8)} ms ║`,
);
console.log(
`║ P99.9 : ${p999.padEnd(8)} ms ║`,
);
console.log(
`║ Max : ${maxL.padEnd(8)} ms ║`,
);
console.log("╠══════════════════════════════════════════════════════════╣");
console.log("║ Reliability ║");
console.log(`║ Successes : ${String(successes).padEnd(40)}║`);
console.log(`║ Error Rate : ${String(errRate + "%").padEnd(40)}║`);
console.log(`║ Timeouts : ${String(timeouts).padEnd(40)}║`);
console.log("╚══════════════════════════════════════════════════════════╝\n");

const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const key = `peak-day-spike-${ts}`;

Expand Down
17 changes: 0 additions & 17 deletions benchmarks/scenarios/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,6 @@ import { check, sleep } from "k6";
import { Rate } from "k6/metrics";

const TARGET_URL = __ENV.TARGET_URL || "http://localhost:3001";
const errorRate = new Rate("smoke_error_rate");

export const options = {
vus: 5,
duration: "1m",
thresholds: {
http_req_duration: ["p(95)<300"],
smoke_error_rate: ["rate<0.01"],
const errorRate = new Rate("smoke_error_rate");

export const options = {
Expand All @@ -35,12 +27,6 @@ export const options = {
function makePayload() {
return JSON.stringify({
event_type: "payment.callback",
provider: "mtn",
reference: `SMOKE-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
amount: 1000.00,
currency: "XAF",
status: "success",
timestamp: new Date().toISOString(),
provider: "mtn",
reference: `SMOKE-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
amount: 1000.0,
Expand All @@ -58,8 +44,6 @@ export default function () {
});

const ok = check(res, {
"status 202": (r) => r.status === 202,
"has reference": (r) => { try { return r.json("reference") !== undefined; } catch { return false; } },
"status 202": (r) => r.status === 202,
"has reference": (r) => {
try {
Expand All @@ -77,7 +61,6 @@ export default function () {

export function handleSummary(data) {
const pass = (data.metrics.smoke_error_rate?.values?.rate ?? 0) < 0.01;
console.log(`\n Smoke test: ${pass ? "✓ PASSED — safe to run peak-day spike" : "✗ FAILED — fix issues before load testing"}\n`);
console.log(
`\n Smoke test: ${pass ? "✓ PASSED — safe to run peak-day spike" : "✗ FAILED — fix issues before load testing"}\n`,
);
Expand Down
Loading
Loading