Andrey Antukh 05fa07f911 Add k6 performance test suite for backend
- run.sh CLI orchestrator with per-script defaults and env-var override
- Shared penpot-client.js library (JSON RPC, cookie auth, tagged metrics)
- Scripts: lifecycle, workspace-open, workspace-edit, concurrent-edit,
  media-upload, font-upload, file-size-matrix, compare-results
- Concurrent-edit supports same-file and multi-file modes via shared teams
- CI workflow (perf-regression) comparing baseline vs PR branch
- k6 binary installed in devenv Dockerfile
2026-06-25 11:29:37 +02:00

129 lines
5.4 KiB
Markdown

# Penpot Performance Tests
k6-based load and performance test suite for the Penpot backend. Measures HTTP RPC latency, throughput, and error rates under synthetic user load.
## Prerequisites
- **k6** — Install from https://k6.io/docs/get-started/installation/ (also included in `devenv` image)
- **Running Penpot backend** — Local devenv (`http://localhost:6060`) or a remote instance
## Quick Start
```bash
# Smoke test — 1 VU, 1 iteration, demo mode
./run.sh smoke
# Full lifecycle with 10 VUs, 5 iterations each
./run.sh lifecycle -v 10 -n 5
# Use registration flow instead of demo profiles
./run.sh lifecycle -m register -v 5 -n 1
# Point to a remote backend
./run.sh lifecycle -u https://penpot.example.com
# Show all options
./run.sh help
```
## Commands
| Command | Description |
|---|---|
| `smoke` | 1 VU, 1 iteration smoke test of the lifecycle flow |
| `lifecycle` | Full user lifecycle (register → CRUD → delete) |
| `workspace-open` | Read-heavy: repeatedly open a file (get-file, libraries, thumbnails) |
| `workspace-edit` | Write-heavy: repeatedly edit a file (get-file + update-file loop) |
| `media-upload` | Upload images of varying sizes (direct + chunked) |
| `font-upload` | Upload fonts via chunked upload + create-font-variant |
| `concurrent-edit` | Concurrent editing: same-file or multi-file mode |
| `file-size-matrix` | Measure latency vs file size (10, 100, 500, 1000 shapes) |
| `compare` | Compare two k6 JSON results for regression |
| `all` | Run all scenarios together (orchestrator) |
| `clean` | Remove test results |
## Options
| Flag | Env Variable | Default | Description |
|---|---|---|---|
| `-u URL` | `PENPOT_BASE_URL` | `http://localhost:6060` | Penpot backend URL |
| `-v NUM` | — | per-script default | Number of virtual users |
| `-n NUM` | — | per-script default | k6 iterations |
| `-d DUR` | `PENPOT_DURATION` | k6 default | Test duration (e.g. `30s`, `5m`, `2h`) |
| `-m MODE` | `PENPOT_REGISTER_MODE` | `demo` | Register mode: `demo` or `register` |
| `-k PATH` | `K6` | `k6` | Path to k6 binary |
### Concurrent-edit / file-size-matrix options
| Flag | Env Variable | Default | Description |
|---|---|---|---|
| `--mode MODE` | `PENPOT_EDIT_MODE` | `same-file` | `same-file` or `multi-file` |
| `--files NUM` | `PENPOT_FILE_COUNT` | `1` | Number of files for multi-file mode |
| `--vus-per-file NUM` | `PENPOT_VUS_PER_FILE` | `1` | VUs per file for multi-file mode |
| `--edit-iterations NUM` | `PENPOT_EDIT_ITERATIONS` | `10` | Per-VU edit loop iterations |
`--edit-iterations` controls the per-VU edit loop in both `concurrent-edit` and `file-size-matrix`. It is **independent** of `-n` (which controls k6's shared-iterations executor).
### Register Modes
- **`demo`** (default): Uses the `create-demo-profile` RPC endpoint. Requires the `demo-users` feature flag to be enabled on the backend. Fastest for testing.
- **`register`**: Uses the full two-step registration flow (`prepare-register-profile` + `register-profile`). Works without any feature flags but is slower.
## Examples
```bash
# Same-file concurrent edit: 5 VUs editing the same file
./run.sh concurrent-edit --mode same-file -v 5 -n 10 --edit-iterations 20
# Multi-file concurrent edit: 3 files, 4 VUs each
./run.sh concurrent-edit --mode multi-file --files 3 --vus-per-file 4 -n 10
# File size matrix: 50 iterations per size tier
./run.sh file-size-matrix --edit-iterations 50
# Duration-based test: 5 VUs for 30 seconds
./run.sh lifecycle -v 5 -d 30s
# Run all scenarios with 50 VUs
./run.sh all -v 50
# Compare baseline vs current results
./run.sh compare results/baseline/20250625-120000-lifecycle/k6-summary.json \
results/current/20250625-130000-lifecycle/k6-summary.json
```
## Shared Client (`lib/penpot-client.js`)
The shared client module wraps the Penpot backend RPC API using plain JSON (not Transit). Key features:
- **JSON transport**: Uses `Content-Type: application/json` for POST bodies and `Accept: application/json` (or `_fmt=json` for GET) for responses.
- **Cookie-based auth**: k6 automatically manages session cookies per VU.
- **Session headers**: Generates `x-session-id` and `x-external-session-id` UUIDs per VU.
- **Tagged metrics**: Every request is tagged with `rpc_command` for k6 metric slicing.
## Results
Test results are written to `results/<timestamp>/` as JSON. k6 also prints a summary to stdout with percentile breakdowns per RPC command.
## Thresholds
Each script includes built-in thresholds that cause k6 to exit with a non-zero code if exceeded:
- `http_req_duration p95 < 5000ms` (global)
- `http_req_failed < 1%` (global)
- Per-command thresholds for login, profile, project, file, and update operations
## Adding New Flows
1. Create `scripts/<flow-name>.js`
2. Import the shared client: `import { createClient } from "../lib/penpot-client.js";`
3. Implement the flow using the client methods
4. Add a command in `run.sh`
## Architecture Notes
- The backend supports both Transit JSON and plain JSON. This test suite uses **plain JSON** for simplicity (no Transit encoder needed in k6).
- JSON request keys are in **kebab-case** (matching Clojure conventions). JSON response keys are in **camelCase** (backend's default JSON encoding).
- `update-file` sends the `id` parameter both in the query string and in the POST body, matching the frontend's behavior.
- The backend uses optimistic concurrency control (`revn`) for file updates. The test retries once on conflict.