Skip to content

Commit ec75718

Browse files
authored
Merge pull request #558 from Ekenesamuel8/kenesam
Add configurable contract load testing suite
2 parents f034074 + e77647b commit ec75718

8 files changed

Lines changed: 874 additions & 36 deletions

File tree

.cargo/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ linker = "clang"
88
linker = "clang"
99

1010
[build]
11-
target = "x86_64-pc-windows-gnu"
11+
target = "x86_64-pc-windows-gnu"

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,47 @@ jobs:
162162
name: perf-report
163163
path: target/perf_report.json
164164

165+
load-testing:
166+
name: Load Testing
167+
runs-on: ubuntu-latest
168+
env:
169+
STRELLER_PEAK_LOAD: 5
170+
STRELLER_LOAD_MULTIPLIER: 10
171+
STRELLER_STUDENT_POOL: 20
172+
STRELLER_COURSE_COUNT: 4
173+
STRELLER_READ_MULTIPLIER: 2
174+
steps:
175+
- uses: actions/checkout@v4
176+
177+
- name: Install Rust toolchain
178+
uses: dtolnay/rust-toolchain@stable
179+
180+
- name: Cache cargo registry
181+
uses: actions/cache@v4
182+
with:
183+
path: |
184+
~/.cargo/registry
185+
~/.cargo/git
186+
target
187+
key: ${{ runner.os }}-cargo-load-${{ hashFiles('**/Cargo.lock') }}
188+
restore-keys: |
189+
${{ runner.os }}-cargo-load-
190+
${{ runner.os }}-cargo-
191+
192+
- name: Make scripts executable
193+
run: chmod +x scripts/*.sh
194+
195+
- name: Run bounded load test suite
196+
run: ./scripts/load_test.sh --ci --report target/load-test-report.json --summary target/load-test-summary.md
197+
198+
- name: Upload load test artifacts
199+
uses: actions/upload-artifact@v4
200+
with:
201+
name: load-test-report
202+
path: |
203+
target/load-test-report.json
204+
target/load-test-summary.md
205+
165206
visual-regression:
166207
name: Visual Regression
167208
runs-on: ubuntu-latest
@@ -248,6 +289,7 @@ jobs:
248289
echo "Test: ${{ needs.test.result }}"
249290
echo "Coverage: ${{ needs.coverage.result }}"
250291
echo "Performance: ${{ needs.performance.result }}"
292+
echo "Load Testing: ${{ needs.load-testing.result }}"
251293
echo "Visual Regression: ${{ needs.visual-regression.result }}"
252294
echo "Accessibility: ${{ needs.accessibility.result }}"
253295
exit 1

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# This Makefile provides convenient commands for development and testing
44

5-
.PHONY: help build test unit-test e2e-test localnet-start localnet-stop localnet-status clean deploy-testnet deploy-mainnet fmt lint lint-style pre-commit-install pre-commit-run coverage coverage-html coverage-lcov coverage-open security-scan security-scan-full perf-profile perf-baseline ci-security ci-coverage ci-perf test-coverage-comprehensive test-coverage-unit test-coverage-integration test-coverage-property test-coverage-edge-cases test-coverage-errors test-coverage-performance test-coverage-report coverage-threshold coverage-badge coverage-dashboard
5+
.PHONY: help build test unit-test e2e-test localnet-start localnet-stop localnet-status clean deploy-testnet deploy-mainnet fmt lint lint-style pre-commit-install pre-commit-run coverage coverage-html coverage-lcov coverage-open security-scan security-scan-full perf-profile perf-baseline load-test load-test-ci ci-security ci-coverage ci-perf test-coverage-comprehensive test-coverage-unit test-coverage-integration test-coverage-property test-coverage-edge-cases test-coverage-errors test-coverage-performance test-coverage-report coverage-threshold coverage-badge coverage-dashboard
66

77
# Colors for output
88
GREEN=\033[0;32m
@@ -57,6 +57,8 @@ help:
5757
@echo " $(RED)security-scan-full$(NC) - Full security scan including semgrep"
5858
@echo " $(RED)perf-profile$(NC) - Performance profile all contracts"
5959
@echo " $(RED)perf-baseline$(NC) - Save performance baseline"
60+
@echo " $(RED)load-test$(NC) - Run configurable contract load tests and write reports"
61+
@echo " $(RED)load-test-ci$(NC) - Run bounded load tests with CI-safe defaults"
6062
@echo ""
6163
@echo "Examples:"
6264
@echo " make e2e-test # Full E2E test cycle"
@@ -267,6 +269,18 @@ perf-compare:
267269
chmod +x ./scripts/perf_profile.sh
268270
./scripts/perf_profile.sh --compare target/perf_baseline.json
269271

272+
# Run the contract load-test suite with local defaults.
273+
load-test:
274+
@echo "$(RED)[LOAD]$(NC) Running load test suite..."
275+
chmod +x ./scripts/load_test.sh
276+
./scripts/load_test.sh
277+
278+
# Run the load-test suite with CI-safe bounded defaults.
279+
load-test-ci:
280+
@echo "$(RED)[LOAD]$(NC) Running CI-safe load test suite..."
281+
chmod +x ./scripts/load_test.sh
282+
./scripts/load_test.sh --ci
283+
270284
# ─────────────────────────────────────────────────────────────
271285
# CI convenience targets
272286
# ─────────────────────────────────────────────────────────────

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@ Run unit tests to ensure everything is functioning as expected:
194194
cargo test
195195
```
196196

197+
#### Load Testing
198+
199+
Run the bounded contract load-testing suite locally:
200+
201+
```bash
202+
./scripts/load_test.sh
203+
```
204+
205+
Or with Make / Cargo:
206+
207+
```bash
208+
make load-test
209+
cargo load-test -- --report target/load-test-report.json --summary target/load-test-summary.md
210+
```
211+
212+
The suite uses a default `STRELLER_LOAD_MULTIPLIER=10` to simulate 10x peak load, writes JSON and Markdown reports to `target/`, and has a CI-safe mode available via `./scripts/load_test.sh --ci`.
213+
214+
See [docs/LOAD_TESTING.md](docs/LOAD_TESTING.md) for the configurable load variables and report format.
215+
197216
#### Property-Based Tests
198217

199218
We use `proptest` to verify contract invariants with random inputs:

docs/LOAD_TESTING.md

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,44 @@
1-
# StrellerMinds Load Testing & Performance Benchmarks
1+
# StrellerMinds Load Testing
22

3-
This document outlines the load testing suite and performance benchmarks implemented for the StrellerMinds smart contracts.
3+
This repository includes a contract load-testing suite for scalability and performance benchmarking. The suite runs in Soroban's host-side test environment, which keeps CI stable while still stressing contract execution paths, storage writes, and repeated reads at a configurable 10x peak multiplier.
44

5-
## 🚀 Overview
5+
## What the suite measures
66

7-
The load testing suite is designed to evaluate how the system behaves under stress, specifically focusing on transaction throughput, resource consumption (CPU/Memory), and latency when multiple users interact with the contracts.
7+
- Latency and duration per scenario
8+
- Throughput in operations per second
9+
- Failed operations
10+
- Emitted contract events
11+
- Estimated persistent state writes
12+
- Process RSS memory delta on Linux runners when available
13+
- Bottleneck notes generated from p95 latency, throughput, and write intensity heuristics
814

9-
## 📁 Structure
15+
## Scenarios
1016

1117
- **E2E Load Tests**: Located in `e2e-tests/tests/load_testing.rs`. These tests run against a live Soroban network (e.g., localnet).
1218
- **Internal Benchmarks**: Located in `contracts/token/src/benchmarks.rs`. These are unit tests that measure execution efficiency within the `soroban-sdk` test environment.
1319
- **CI/CD Integration**: Automated daily runs via GitHub Actions (`.github/workflows/load-testing.yml`) to track baseline metrics and alert on performance regressions.
1420

15-
## 🧪 Load Scenarios
21+
1. `progress-write-hot-path`
22+
Runs repeated `record_progress` calls against a tight course set to stress hot writes.
23+
2. `progress-write-multi-course`
24+
Spreads `record_progress` calls across more courses to grow student course lists and storage churn.
25+
3. `progress-read-heavy`
26+
Seeds progress data, then benchmarks `get_progress` and `get_student_courses` under sustained reads.
1627

1728
### 1. High Volume Analytics Recording
1829
- **Scenario**: Simulates multiple students recording learning sessions simultaneously.
1930
- **Component**: `analytics` contract.
2031
- **Goal**: Measure throughput (ops/sec) and ensure non-blocking transaction flow.
2132
- **Running**: `LOAD_TEST_REQUESTS=10000 cargo test --test load_testing test_load_analytics_recording_stress -- --ignored`
2233

23-
### 2. Leaderboard Generation Performance
24-
- **Scenario**: Measures the time taken to generate leaderboards with a large set of student data.
25-
- **Component**: `analytics` contract.
26-
- **Goal**: Ensure leaderboard sorting and calculation remain efficient as the user base grows.
27-
- **Running**: `cargo test --test load_testing test_load_leaderboard_generation_performance -- --ignored`
34+
The runner simulates:
2835

29-
### 3. Diagnostics Monitoring Overhead
30-
- **Scenario**: Compares transaction execution time with and without active diagnostics monitoring.
31-
- **Component**: `diagnostics` & `token` contracts.
32-
- **Goal**: Quantify the performance "cost" of real-time monitoring.
33-
- **Running**: `cargo test --test load_testing test_load_diagnostics_overhead -- --ignored`
36+
- `peak load * load multiplier` write operations
37+
- `(peak load * load multiplier) * read multiplier` read operations
3438

35-
### 4. Token Mint/Transfer Load
36-
- **Scenario**: Performs 100+ mint and transfer operations in a single test environment.
37-
- **Component**: `token` contract.
38-
- **Goal**: Benchmark gas efficiency and CPU budget utilization.
39-
- **Running**: `cargo test -p token benchmark_token_load`
39+
By default, the multiplier is `10`, which gives the required 10x peak simulation. All values are configurable through environment variables.
4040

41-
## 📊 Success Criteria
41+
## Local usage
4242

4343
| Metric | Target |
4444
| :--- | :--- |
@@ -47,20 +47,87 @@ The load testing suite is designed to evaluate how the system behaves under stre
4747
| Diagnostics Overhead | < 15% CPU increase |
4848
| Recovery Time | < 5s after surge |
4949

50-
## 🛠️ Monitoring Load Performance
50+
```bash
51+
./scripts/load_test.sh
52+
```
53+
54+
Or through cargo:
55+
56+
```bash
57+
cargo load-test -- --report target/load-test-report.json --summary target/load-test-summary.md
58+
```
59+
60+
Or through Make:
61+
62+
```bash
63+
make load-test
64+
```
65+
66+
## CI-safe usage
67+
68+
The CI workflow uses bounded defaults to keep the job deterministic:
69+
70+
```bash
71+
./scripts/load_test.sh --ci
72+
```
73+
74+
Or:
75+
76+
```bash
77+
make load-test-ci
78+
```
79+
80+
## Environment variables
81+
82+
| Variable | Purpose | Local Default | CI Default |
83+
| --- | --- | ---: | ---: |
84+
| `STRELLER_PEAK_LOAD` | Baseline peak load before multiplying | `25` | `5` |
85+
| `STRELLER_LOAD_MULTIPLIER` | Stress multiplier | `10` | `10` |
86+
| `STRELLER_STUDENT_POOL` | Number of simulated students | `50` | `20` |
87+
| `STRELLER_COURSE_COUNT` | Number of simulated courses | `8` | `4` |
88+
| `STRELLER_READ_MULTIPLIER` | Read operations per seeded write | `3` | `2` |
89+
90+
Example:
91+
92+
```bash
93+
STRELLER_PEAK_LOAD=40 \
94+
STRELLER_LOAD_MULTIPLIER=10 \
95+
STRELLER_STUDENT_POOL=80 \
96+
STRELLER_COURSE_COUNT=12 \
97+
./scripts/load_test.sh
98+
```
99+
100+
## Reports
101+
102+
Each run produces:
103+
104+
- `target/load-test-report.json` - machine-readable metrics for CI artifacts
105+
- `target/load-test-summary.md` - human-readable scenario summary and bottleneck notes
106+
107+
You can override both paths with:
108+
109+
```bash
110+
./scripts/load_test.sh --report target/custom-load-report.json --summary target/custom-load-summary.md
111+
```
112+
113+
## Reading the output
114+
115+
Focus on these fields first:
116+
117+
- `throughput_ops_per_sec`
118+
- `avg_latency_ms`
119+
- `p95_latency_ms`
120+
- `failed_operations`
121+
- `estimated_state_writes`
51122

52-
We use the `diagnostics` contract to monitor performance during load tests. Key metrics tracked include:
53-
- `average_execution_time`: Average time per transaction.
54-
- `gas_used`: Resource consumption per operation.
55-
- `error_rate`: Percentage of failed transactions under load.
123+
If the generated bottleneck notes mention tail latency or write intensity, the usual next step is to inspect storage-heavy contract code paths, symbol creation patterns, and list growth in persistent storage.
56124

57-
## 📝 Reviewing Effectiveness
125+
## CI integration
58126

59-
Load tests should be reviewed:
60-
1. **After major refactors**: To ensure no performance regressions.
61-
2. **Before mainnet deployment**: To validate system capacity.
62-
3. **When adding new features**: To check impact on existing throughput.
127+
The main CI workflow includes a dedicated `Load Testing` job that:
63128

64-
---
129+
1. Uses bounded environment defaults
130+
2. Runs `./scripts/load_test.sh --ci`
131+
3. Uploads the JSON and Markdown reports as workflow artifacts
65132

66-
*Verified on: 2026-03-27*
133+
This keeps the suite safe for pull requests while still catching regressions in latency, throughput, and storage-heavy paths.

scripts/load_test.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6+
REPORT_PATH="${ROOT_DIR}/target/load-test-report.json"
7+
SUMMARY_PATH="${ROOT_DIR}/target/load-test-summary.md"
8+
CI_MODE=0
9+
10+
while [[ $# -gt 0 ]]; do
11+
case "$1" in
12+
--report)
13+
shift
14+
REPORT_PATH="${1:?missing report path}"
15+
shift
16+
;;
17+
--summary)
18+
shift
19+
SUMMARY_PATH="${1:?missing summary path}"
20+
shift
21+
;;
22+
--ci)
23+
CI_MODE=1
24+
shift
25+
;;
26+
-h|--help)
27+
cat <<'EOF'
28+
StrellerMinds load-test wrapper
29+
30+
Usage:
31+
./scripts/load_test.sh [--ci] [--report <path>] [--summary <path>]
32+
33+
Environment:
34+
STRELLER_PEAK_LOAD
35+
STRELLER_LOAD_MULTIPLIER
36+
STRELLER_STUDENT_POOL
37+
STRELLER_COURSE_COUNT
38+
STRELLER_READ_MULTIPLIER
39+
EOF
40+
exit 0
41+
;;
42+
*)
43+
echo "Unknown option: $1" >&2
44+
exit 1
45+
;;
46+
esac
47+
done
48+
49+
if [[ "${CI_MODE}" -eq 1 ]]; then
50+
export STRELLER_PEAK_LOAD="${STRELLER_PEAK_LOAD:-5}"
51+
export STRELLER_LOAD_MULTIPLIER="${STRELLER_LOAD_MULTIPLIER:-10}"
52+
export STRELLER_STUDENT_POOL="${STRELLER_STUDENT_POOL:-20}"
53+
export STRELLER_COURSE_COUNT="${STRELLER_COURSE_COUNT:-4}"
54+
export STRELLER_READ_MULTIPLIER="${STRELLER_READ_MULTIPLIER:-2}"
55+
RUNNER_ARGS=(--ci)
56+
else
57+
export STRELLER_PEAK_LOAD="${STRELLER_PEAK_LOAD:-25}"
58+
export STRELLER_LOAD_MULTIPLIER="${STRELLER_LOAD_MULTIPLIER:-10}"
59+
export STRELLER_STUDENT_POOL="${STRELLER_STUDENT_POOL:-50}"
60+
export STRELLER_COURSE_COUNT="${STRELLER_COURSE_COUNT:-8}"
61+
export STRELLER_READ_MULTIPLIER="${STRELLER_READ_MULTIPLIER:-3}"
62+
RUNNER_ARGS=()
63+
fi
64+
65+
mkdir -p "$(dirname "${REPORT_PATH}")"
66+
mkdir -p "$(dirname "${SUMMARY_PATH}")"
67+
68+
cd "${ROOT_DIR}"
69+
cargo run -p load-test-runner -- "${RUNNER_ARGS[@]}" --report "${REPORT_PATH}" --summary "${SUMMARY_PATH}"

utils/load-test-runner/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "load-test-runner"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "Apache-2.0"
6+
7+
[dependencies]
8+
progress = { path = "../../contracts/progress" }
9+
soroban-sdk = { workspace = true, features = ["testutils"] }
10+
serde = { version = "1.0", features = ["derive"] }
11+
serde_json = "1.0"

0 commit comments

Comments
 (0)