perf-skills 2.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -6
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +216 -187
- package/package.json +7 -2
- package/skills/perf/SKILL.md +60 -33
- package/skills/perf/evals/evals.json +80 -0
- package/skills/perf/evals/trigger-eval.json +38 -0
- package/skills/perf/references/tools/artillery.md +331 -0
- package/skills/perf/references/tools/loadrunner.md +24 -0
- package/skills/perf/references/tools/neoload.md +54 -0
- package/skills/perf/references/topics/correlation.md +4 -0
- package/skills/perf/references/topics/database-testing.md +2 -0
- package/skills/perf/references/topics/llm-inference.md +179 -0
- package/skills/perf/references/topics/modern-architectures.md +2 -0
- package/skills/perf/references/topics/observability.md +2 -0
- package/skills/perf/references/topics/production-testing.md +2 -0
- package/skills/perf/references/topics/protocol-testing.md +65 -0
- package/skills/perf/references/topics/results-analysis.md +2 -0
- package/skills/perf/references/topics/script-generation.md +10 -17
- package/skills/perf/references/topics/slo-capacity.md +136 -0
- package/skills/perf/references/topics/test-data.md +2 -0
- package/skills/perf/references/topics/test-execution.md +32 -5
|
@@ -57,5 +57,59 @@ curl -X POST "https://neoload-api.tricentis.com/v3/tests/{testId}/start" \
|
|
|
57
57
|
- Integrate with **Dynatrace/AppDynamics** via built-in APM connectors.
|
|
58
58
|
- Use **as-code YAML definitions** (NeoLoad as Code) for version-controlled test configs.
|
|
59
59
|
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## NeoLoad as Code (YAML)
|
|
63
|
+
|
|
64
|
+
Version-control your test definitions instead of relying on the GUI `.nlp` file:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# neoload-project.yml
|
|
68
|
+
name: Checkout-Load-Test
|
|
69
|
+
populations:
|
|
70
|
+
- name: Checkout_Users
|
|
71
|
+
user_paths:
|
|
72
|
+
- name: Checkout_Flow
|
|
73
|
+
distribution: 70%
|
|
74
|
+
- name: Browse_Flow
|
|
75
|
+
distribution: 30%
|
|
76
|
+
|
|
77
|
+
scenarios:
|
|
78
|
+
- name: Regression_Load
|
|
79
|
+
populations:
|
|
80
|
+
- name: Checkout_Users
|
|
81
|
+
load_policy:
|
|
82
|
+
steps:
|
|
83
|
+
- type: ramp
|
|
84
|
+
from: 0
|
|
85
|
+
to: 200
|
|
86
|
+
duration: 120s
|
|
87
|
+
- type: constant
|
|
88
|
+
value: 200
|
|
89
|
+
duration: 600s
|
|
90
|
+
- type: ramp
|
|
91
|
+
from: 200
|
|
92
|
+
to: 0
|
|
93
|
+
duration: 60s
|
|
94
|
+
|
|
95
|
+
sla_profiles:
|
|
96
|
+
- name: Checkout_SLA
|
|
97
|
+
thresholds:
|
|
98
|
+
- per_transaction: Checkout
|
|
99
|
+
avg_response_time:
|
|
100
|
+
warn: 500ms
|
|
101
|
+
fail: 1000ms
|
|
102
|
+
error_rate:
|
|
103
|
+
warn: 1%
|
|
104
|
+
fail: 5%
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Validate and run from CLI
|
|
109
|
+
neoload-cli validate neoload-project.yml
|
|
110
|
+
neoload-cli run --scenario Regression_Load
|
|
111
|
+
```
|
|
112
|
+
|
|
60
113
|
> For anti-patterns, assertions, think time, and parameterization principles, see **Key Principles** in `SKILL.md`.
|
|
61
114
|
> For CI/CD integration details, see `../topics/test-execution.md`.
|
|
115
|
+
> For SLO threshold design, see `../topics/slo-capacity.md`.
|
|
@@ -404,3 +404,7 @@ If you need help configuring correlation, copy and paste the following prompt te
|
|
|
404
404
|
3. Write the exact extraction syntax for the target tool.
|
|
405
405
|
4. Show the exact injection syntax (including variable syntax, e.g., `${varName}`, `#{varName}`, `{varName}`).
|
|
406
406
|
5. Explain how to debug and verify that the correlation succeeded.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
> **See also:** Tool-specific extractor syntax in `../tools/jmeter.md`, `../tools/k6.md`, `../tools/gatling.md`, `../tools/locust.md`, `../tools/loadrunner.md`, `../tools/artillery.md`. Script-level correlation patterns in `script-generation.md`.
|
|
@@ -212,3 +212,5 @@ WHERE NOT bl.granted;
|
|
|
212
212
|
- [ ] Replication lag monitoring active (if using read replicas)
|
|
213
213
|
- [ ] Connection pool metrics exposed and dashboarded
|
|
214
214
|
- [ ] Cleanup script ready for test data removal post-test
|
|
215
|
+
|
|
216
|
+
> **See also:** JDBC sampler setup in `../tools/jmeter.md`. Monitoring and dashboards in `observability.md`. Interpreting saturation and bottleneck signals in `results-analysis.md`.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# LLM Inference Performance
|
|
2
|
+
|
|
3
|
+
> Scope: load and capacity testing of LLM inference servers (vLLM, TRT-LLM, SGLang, OpenAI-compatible endpoints, Ray Serve LLM, KServe). Covers metric definitions, workload design, tools, and SLO methodology for generative AI.
|
|
4
|
+
|
|
5
|
+
LLM inference is **not conventional HTTP load testing**. Responses stream token-by-token over an open connection, output length is unbounded, and the dominant cost is GPU compute, not network. A generic `k6`/`JMeter` run that only measures request latency will badly misreport performance - it ignores the streaming shape entirely. This topic defines the metrics that actually matter, how to design realistic LLM workloads, and which tools to use.
|
|
6
|
+
|
|
7
|
+
> For general workload theory (Little's Law, open vs closed models), see `workload-design.md`. For analyzing the percentiles and SLO failures this produces, see `results-analysis.md`. For running these tests in CI/CD, see `test-execution.md`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why LLM load testing is different
|
|
12
|
+
|
|
13
|
+
| Aspect | Traditional API | LLM inference |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Response | Single body, fixed size | Streamed tokens, variable length |
|
|
16
|
+
| Latency signal | TTFB + total | TTFT + inter-token gaps + E2E |
|
|
17
|
+
| Cost driver | CPU/network | GPU memory (KV cache), compute |
|
|
18
|
+
| Throughput unit | Requests/sec | Tokens/sec (not requests/sec) |
|
|
19
|
+
| Concurrency limit | Threads/sockets | KV cache capacity (`max_num_seqs`) |
|
|
20
|
+
| Key risk | Timeouts | Queueing delay, GPU saturation, OOM |
|
|
21
|
+
|
|
22
|
+
Two phases dominate every request:
|
|
23
|
+
- **Prefill** - the model processes the full prompt to build the KV cache. Compute-heavy, determines **TTFT**. Scales with prompt length.
|
|
24
|
+
- **Decode** - tokens generated one at a time using the KV cache. Memory-bandwidth-bound, determines **ITL/TPOT**. Scales with output length.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Metrics
|
|
29
|
+
|
|
30
|
+
### TTFT - Time to First Token
|
|
31
|
+
Time from request send to first streamed token. Driven by prompt length, queue/self.time, and prefill speed. The primary *perceived responsiveness* metric for chat/coding assistants. High TTFT under load usually means the scheduler is queuing requests (KV cache exhausted) rather than a slow model.
|
|
32
|
+
|
|
33
|
+
### TPOT / ITL - Generation smoothness
|
|
34
|
+
- **TPOT (Time Per Output Token)** - average gap between tokens for a single request: `(E2E - TTFT) / (output_tokens - 1)`. Report the mean of per-request TPOTs.
|
|
35
|
+
- **ITL (Inter-Token Latency)** - same gaps, but token-weighted across all requests (mean of every gap). This is the system's steady streaming speed.
|
|
36
|
+
- **Which to use**: TPOT compares per-request behavior (each request equal); ITL estimates system-wide streaming feel across mixed traffic. A bursty ITL (high p99 vs mean) makes output appear in clumps - the classic "GPU contention" signature.
|
|
37
|
+
|
|
38
|
+
### E2E Latency
|
|
39
|
+
Total time from prompt to final token. `E2E = TTFT + (output_tokens - 1) * TPOT`. Matters for batch/codegen/summarization where the full response is needed before the next step.
|
|
40
|
+
|
|
41
|
+
### TPS - Token Throughput
|
|
42
|
+
- **System TPS** - total output tokens/sec across all requests. Raw capacity; rises with load until GPU-saturated. `TPS = output_tokens / (T_last - T_first)`.
|
|
43
|
+
- **User TPS** - tokens/sec a single user experiences; `≈ 1 / ITL` at long outputs. Drops as concurrency rises because the engine shares the GPU.
|
|
44
|
+
|
|
45
|
+
### RPS - Request Throughput
|
|
46
|
+
Completed requests/sec. Dominant metric for high-volume short-prompt traffic (chatbots, search, API gateways). Shorter prompts + higher `max_num_seqs` raise RPS.
|
|
47
|
+
|
|
48
|
+
### Goodput (the metric that matters most)
|
|
49
|
+
`Goodput = (requests meeting ALL SLOs) / total_requests * 100%`. Unlike TPS/RPS, goodput tells you what fraction of users got an *acceptable* experience. A system can show high TPS while most requests violate latency SLOs. Always define goodput with explicit thresholds, e.g. TTFT < 500ms, TPOT < 15ms, E2E < 2s. As load climbs, goodput falls even as raw throughput keeps rising - that crossover is your real capacity limit.
|
|
50
|
+
|
|
51
|
+
### Percentiles
|
|
52
|
+
Report **p50/p95/p99** for TTFT, ITL, and E2E - never just averages. Averages hide the unlucky 5% whose tokens arrive in bursts. p99 is the near-worst-case; if p99 meets SLO, the system is consistent.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Workload Dimensions
|
|
57
|
+
|
|
58
|
+
Controlling these is what separates a useful LLM test from a misleading one:
|
|
59
|
+
|
|
60
|
+
| Dimension | Effect | How to set |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| **Prompt length (ISL)** | Longer → higher TTFT, more KV cache | Use real traffic (ShareGPT) or synthetic ranges |
|
|
63
|
+
| **Output length (OSL)** | Longer → higher E2E, more decode cost | Real distributions; `--random-output-len` for synthetic |
|
|
64
|
+
| **Concurrency** | Bounded by KV cache, not sockets | Set `--max-concurrency` to simulate gateway limits |
|
|
65
|
+
| **Request rate** | Open-model arrival (Poisson) | `--request-rate`; `inf` for max throughput |
|
|
66
|
+
| **Burstiness** | Gamma-distributed arrivals | `1.0` realistic, `0.1-0.5` stress, `2-5` uniform |
|
|
67
|
+
| **Batching (`max_num_seqs`)** | Higher → more RPS, worse per-user latency | Tune on the server, not the client |
|
|
68
|
+
|
|
69
|
+
**KV cache math**: `max_concurrency ≈ KV_cache_tokens / max_model_len`. vLLM prints this at startup. Set test concurrency to 80-90% of it for capacity planning; use the full value as the SLA limit.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
|
|
75
|
+
Purpose-built LLM benchmarkers surface token metrics natively. Prefer them over generic HTTP load testers.
|
|
76
|
+
|
|
77
|
+
| Tool | Best for | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| **vLLM `bench serve`** | Serving benchmarks against an OpenAI-compatible endpoint | Native TTFT/TPOT/ITL/TPS, ShareGPT + synthetic datasets, ramp-up, goodput via percentiles |
|
|
80
|
+
| **GuideLLM** | Production vLLM SLA/capacity studies | Auto reports, live progress, profile-based; recommended by vLLM for production |
|
|
81
|
+
| **NVIDIA GenAI-Perf** | Multi-backend (TRT-LLM, Triton, vLLM), concurrency/rate modes | Emits TTFT, ITL, output TPS, goodput; pairs with Perf Analyzer |
|
|
82
|
+
| **llmperf / LLM-Perf** | Quick pointwise latency/throughput checks | Lightweight, great for smoke and regression |
|
|
83
|
+
| **LLM Locust** | Distributed load on Locust, GenAI metrics | Use when you already run Locust fleets |
|
|
84
|
+
| **k6 + custom metrics** | Unified CI with existing k6 stacks | Must instrument streaming manually: capture TTFT from first SSE chunk, ITL between chunks, count tokens. See `../tools/k6.md` for metric primitives |
|
|
85
|
+
|
|
86
|
+
> **Anti-pattern (per current research):** do not misuse model-server micro-benchmarkers (vLLM `bench`, SGLang bench) as production-level evaluators. They optimize for regression/feature testing, not realistic arrival patterns. Use them for component baselines; use GuideLLM/GenAI-Perf/production-style load for capacity and SLO validation.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Quick Start Commands
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# vLLM bench serve - max throughput probe with ShareGPT data
|
|
94
|
+
python -m vllm.entrypoints.openai.api_server --model meta-llama/Llama-3-8B &
|
|
95
|
+
python -m vllm bench serve \
|
|
96
|
+
--model meta-llama/Llama-3-8B \
|
|
97
|
+
--endpoint http://localhost:8000/v1/completions \
|
|
98
|
+
--dataset-name sharegpt \
|
|
99
|
+
--request-rate inf \
|
|
100
|
+
--max-concurrency 256 \
|
|
101
|
+
--num-prompts 500
|
|
102
|
+
|
|
103
|
+
# vLLM bench serve - realistic arrival pattern
|
|
104
|
+
python -m vllm bench serve \
|
|
105
|
+
--model meta-llama/Llama-3-8B \
|
|
106
|
+
--endpoint http://localhost:8000/v1/completions \
|
|
107
|
+
--dataset-name sharegpt \
|
|
108
|
+
--request-rate 10 \
|
|
109
|
+
--burstiness 1.0 \
|
|
110
|
+
--num-prompts 200
|
|
111
|
+
|
|
112
|
+
# GuideLLM - production SLA study
|
|
113
|
+
pip install guidedllm
|
|
114
|
+
guidedllm evaluate \
|
|
115
|
+
--model meta-llama/Llama-3-8B \
|
|
116
|
+
--base-url http://localhost:8000/v1 \
|
|
117
|
+
--data emulated --rate 10 --duration 120
|
|
118
|
+
|
|
119
|
+
# NVIDIA GenAI-Perf - multi-backend benchmark
|
|
120
|
+
genai-perf profile \
|
|
121
|
+
--model meta-llama/Llama-3-8B \
|
|
122
|
+
--endpoint-type chat \
|
|
123
|
+
--url localhost:8000 \
|
|
124
|
+
--concurrency 64 \
|
|
125
|
+
--request-count 200
|
|
126
|
+
|
|
127
|
+
# llmperf - quick latency/throughput check
|
|
128
|
+
python token_benchmark_ray.py \
|
|
129
|
+
--model meta-llama/Llama-3-8B \
|
|
130
|
+
--mean-input-tokens 512 \
|
|
131
|
+
--mean-output-tokens 128 \
|
|
132
|
+
--num-concurrent-requests 32 \
|
|
133
|
+
--results-dir ./results
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Test Design & Methodology
|
|
139
|
+
|
|
140
|
+
1. **Start from real traffic shapes.** Use ShareGPT or captured production traces for prompt/output length distributions. Synthetic `random` datasets are fine for stress but unrealistic for sizing.
|
|
141
|
+
2. **Define SLOs as goodput thresholds first.** e.g. "TTFT p95 < 500ms, ITL p95 < 20ms, E2E p95 < 3s" → goodput target 99%.
|
|
142
|
+
3. **Run a max-throughput probe** (`--request-rate inf --max-concurrency <limit>`) to find the concurrency ceiling and baseline TPS.
|
|
143
|
+
4. **Sweep concurrency / request rate** to find the goodput cliff - the load where SLO compliance drops. That is capacity.
|
|
144
|
+
5. **Test ramp-up and spikes** (linear/exponential ramp, bursty arrival) to validate autoscaling and queue behavior.
|
|
145
|
+
6. **Always assert on token metrics, not just HTTP 200.** A 200 with a 10s TTFT is a failed request.
|
|
146
|
+
|
|
147
|
+
### Workload pattern recipes (vLLM bench semantics)
|
|
148
|
+
| Goal | `--request-rate` | `--burstiness` | `--max-concurrency` |
|
|
149
|
+
|---|---|---|---|
|
|
150
|
+
| Max throughput | `inf` | n/a | limited |
|
|
151
|
+
| Realistic baseline | 5-20 | 1.0 | inf |
|
|
152
|
+
| Stress / resilience | 20-100 | 0.1-0.5 | inf |
|
|
153
|
+
| Latency profiling | 1-10 | 2-5 | inf |
|
|
154
|
+
| Capacity / SLA | target rate | 1.0 | SLA limit |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Common Mistakes
|
|
159
|
+
|
|
160
|
+
- **Measuring only HTTP latency** - TTFT and streaming gaps are invisible to `http_req_duration`. You'll report a green test while users stare at a blank cursor.
|
|
161
|
+
- **Ignoring output length variance** - fixed `max_tokens` hides the real E2E spread; use realistic OSL distributions.
|
|
162
|
+
- **Unbounded concurrency** - without `--max-concurrency`, the client fires until the GPU chokes; you measure collapse, not capacity. The KV cache, not sockets, is the real cap.
|
|
163
|
+
- **Confusing TPS with user experience** - high system TPS can coexist with terrible per-user ITL. Track both; report goodput.
|
|
164
|
+
- **Averaging instead of percentiles** - a 200ms mean TTFT with 8s p99 is a broken chat UX.
|
|
165
|
+
- **Using a micro-benchmarker for production SLOs** - `vllm bench`/SGLang bench validate the engine, not your serving capacity under real arrival patterns.
|
|
166
|
+
- **No streaming instrumentation in k6** - a naive `http.get` to a streaming endpoint counts only the final byte; you must parse SSE chunks to get TTFT/ITL.
|
|
167
|
+
- **Forgetting prompt caching** - prefix caching dramatically cuts TTFT for repeated prefixes; test with and without it (e.g. RAG, system prompts) to size the win.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Observability
|
|
172
|
+
|
|
173
|
+
Track these server-side alongside the client metrics above:
|
|
174
|
+
- **KV cache utilization** - the true saturation signal; when it pins at 100%, TTFT inflates via queueing.
|
|
175
|
+
- **Batch size / `num_running` vs `num_waiting`** - waiting > 0 means you're over concurrency.
|
|
176
|
+
- **Prefill vs decode time split** - isolates whether slowness is prompt-side or generation-side.
|
|
177
|
+
- **GPU util + memory** - confirms you're compute-bound, not starved.
|
|
178
|
+
|
|
179
|
+
> For dashboards/APM integration patterns, see `observability.md`. For interpreting the goodput cliff and saturation, see `results-analysis.md`.
|
|
@@ -312,3 +312,5 @@ Define performance budgets in CI to prevent regressions:
|
|
|
312
312
|
- [ ] Concurrency limits and provisioned concurrency validated
|
|
313
313
|
- [ ] Frontend Core Web Vitals measured under backend load
|
|
314
314
|
- [ ] Performance budgets defined and enforced in CI
|
|
315
|
+
|
|
316
|
+
> **See also:** CI/CD pipeline patterns in `test-execution.md`. Kubernetes and mesh monitoring in `observability.md`. SLO headroom for autoscaling decisions in `slo-capacity.md`.
|
|
@@ -199,3 +199,5 @@ Synthetic monitors catch regressions that slip through staging without requiring
|
|
|
199
199
|
- [ ] Log aggregation active and searchable
|
|
200
200
|
- [ ] Baseline metrics screenshot taken before test starts (for comparison)
|
|
201
201
|
- [ ] Metrics retention configured to keep results for trend analysis
|
|
202
|
+
|
|
203
|
+
> **See also:** k6 metric primitives in `../tools/k6.md`. JMeter Backend Listener in `../tools/jmeter.md`. Interpreting the metrics in `results-analysis.md`. SLO thresholds in `slo-capacity.md`.
|
|
@@ -212,3 +212,5 @@ If something breaks during a test:
|
|
|
212
212
|
4. **Roll back if needed** - restore previous version, restart services.
|
|
213
213
|
5. **Preserve evidence** - don't restart services without capturing logs and heap dumps.
|
|
214
214
|
6. **Post-mortem** - document what broke, at what load, what the root cause was.
|
|
215
|
+
|
|
216
|
+
> **See also:** CI/CD execution patterns in `test-execution.md`. Monitoring during production tests in `observability.md`. SLO and error budget framing in `slo-capacity.md`.
|
|
@@ -58,6 +58,36 @@ export default function () {
|
|
|
58
58
|
- **Connection reuse**: Keep connections open across iterations; don't connect/close per request.
|
|
59
59
|
- **Streaming throughput**: For server-streaming RPCs, measure messages-per-second, not just request latency.
|
|
60
60
|
- **Deadline propagation**: Set gRPC deadlines in tests to match production timeouts.
|
|
61
|
+
|
|
62
|
+
### Gatling gRPC Example
|
|
63
|
+
|
|
64
|
+
```scala
|
|
65
|
+
// Requires: gatling-grpc plugin (io.gatling:gatling-grpc)
|
|
66
|
+
import io.gatling.core.Predef._
|
|
67
|
+
import io.gatling.grpc.Predef._
|
|
68
|
+
import io.grpc.ManagedChannelBuilder
|
|
69
|
+
import scala.concurrent.duration._
|
|
70
|
+
|
|
71
|
+
class GrpcSimulation extends Simulation {
|
|
72
|
+
val channel = ManagedChannelBuilder
|
|
73
|
+
.forAddress("grpc-server", 50051)
|
|
74
|
+
.usePlaintext()
|
|
75
|
+
.build()
|
|
76
|
+
|
|
77
|
+
val scn = scenario("gRPC Unary")
|
|
78
|
+
.exec(
|
|
79
|
+
grpc("SayHello")
|
|
80
|
+
.rpc(HelloServiceGrpc.METHOD_SAY_HELLO)
|
|
81
|
+
.payload(HelloRequest(greeting = "perf-test"))
|
|
82
|
+
.check(statusCode.is(StatusCode.OK))
|
|
83
|
+
)
|
|
84
|
+
.pause(1)
|
|
85
|
+
|
|
86
|
+
setUp(
|
|
87
|
+
scn.injectOpen(rampUsers(100).during(30.seconds))
|
|
88
|
+
).protocols(grpc(channel))
|
|
89
|
+
}
|
|
90
|
+
```
|
|
61
91
|
- **Load balancer awareness**: gRPC over HTTP/2 with persistent connections can cause uneven load across backends - test with client-side load balancing or L7 proxy.
|
|
62
92
|
- **Protobuf payload size**: Binary encoding is smaller than JSON - adjust throughput expectations accordingly.
|
|
63
93
|
|
|
@@ -199,6 +229,39 @@ export default function () {
|
|
|
199
229
|
3. **Measure server-side**: File descriptor count, memory per connection, event loop lag.
|
|
200
230
|
4. **Test reconnection storms**: Kill server, observe client reconnection behavior and server recovery.
|
|
201
231
|
|
|
232
|
+
### Gatling WebSocket Example
|
|
233
|
+
|
|
234
|
+
```scala
|
|
235
|
+
import io.gatling.core.Predef._
|
|
236
|
+
import io.gatling.http.Predef._
|
|
237
|
+
import scala.concurrent.duration._
|
|
238
|
+
|
|
239
|
+
class WebSocketSimulation extends Simulation {
|
|
240
|
+
val httpProtocol = http.baseUrl("https://app.example.com")
|
|
241
|
+
|
|
242
|
+
val scn = scenario("WebSocket Chat")
|
|
243
|
+
.exec(
|
|
244
|
+
ws("Connect WS")
|
|
245
|
+
.connect("/ws/chat")
|
|
246
|
+
.header("Authorization", "Bearer #{token}")
|
|
247
|
+
)
|
|
248
|
+
.exec(
|
|
249
|
+
ws("Subscribe")
|
|
250
|
+
.sendText("""{"type":"subscribe","channel":"updates"}""")
|
|
251
|
+
.await(30.seconds)(
|
|
252
|
+
ws.checkTextMessage("subscribed")
|
|
253
|
+
.check(bodyJsonPath("$.type").is("subscribed"))
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
.pause(30) // hold connection open
|
|
257
|
+
.exec(ws("Close").close)
|
|
258
|
+
|
|
259
|
+
setUp(
|
|
260
|
+
scn.injectOpen(rampUsers(500).during(60.seconds))
|
|
261
|
+
).protocols(httpProtocol)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
202
265
|
---
|
|
203
266
|
|
|
204
267
|
## Message Queue / Event Streaming Testing
|
|
@@ -272,3 +335,5 @@ export function teardown() {
|
|
|
272
335
|
- [ ] Backpressure / flow control behavior validated
|
|
273
336
|
- [ ] Error codes and retry behavior tested (gRPC status codes, AMQP nacks, etc.)
|
|
274
337
|
- [ ] End-to-end latency measured (not just request latency)
|
|
338
|
+
|
|
339
|
+
> **See also:** k6 scripting (all examples above) in `../tools/k6.md`. Gatling gRPC/WebSocket in `../tools/gatling.md`. JMeter JMS/plugins in `../tools/jmeter.md`. Workload design for streaming protocols in `workload-design.md`.
|
|
@@ -242,3 +242,5 @@ Structure your report:
|
|
|
242
242
|
- Error log samples
|
|
243
243
|
- Environment configuration
|
|
244
244
|
```
|
|
245
|
+
|
|
246
|
+
> **See also:** Metric collection and dashboards in `observability.md`. SLO interpretation and error budgets in `slo-capacity.md`. LLM-specific metrics (TTFT, TPOT, goodput) in `llm-inference.md`.
|
|
@@ -39,26 +39,17 @@ Record a HAR file from the browser → import into your tool → clean up and pa
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
## Correlation
|
|
42
|
+
## Correlation
|
|
43
43
|
|
|
44
44
|
Correlation is extracting a dynamic value from a response and using it in a subsequent request. Missing correlation is the #1 cause of performance script failure.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
- **Timestamps/signatures** used in request signing
|
|
54
|
-
|
|
55
|
-
### Correlation debugging approach
|
|
56
|
-
1. Run the script with 1 VU.
|
|
57
|
-
2. Look for HTTP 4xx errors (especially 403 Forbidden, 422 Unprocessable Entity).
|
|
58
|
-
3. Check the request body/headers - is a token missing or stale?
|
|
59
|
-
4. Use the tool's debug output or proxy (Fiddler, Charles) to inspect live traffic.
|
|
60
|
-
5. Identify where the value appears in a *previous* response.
|
|
61
|
-
6. Add an extractor at that response, reference the variable in the failing request.
|
|
46
|
+
> **Full reference:** see `correlation.md` for the complete correlation workflow, tool-specific extractor syntax, tech-stack rules (ASP.NET, Java, SAP, OAuth), brittle-vs-robust regex guidance, and the diagnostic playbook.
|
|
47
|
+
|
|
48
|
+
### Quick checklist
|
|
49
|
+
- Session tokens, CSRF tokens, OAuth codes, resource IDs, and one-time nonces must always be correlated.
|
|
50
|
+
- Run with 1 VU first; 403/422 errors usually mean a missing or stale token.
|
|
51
|
+
- Use a proxy (Fiddler, Charles, mitmproxy) to find where the value appears in a previous response.
|
|
52
|
+
- Add the extractor at that response, reference the variable in the failing request.
|
|
62
53
|
|
|
63
54
|
---
|
|
64
55
|
|
|
@@ -219,3 +210,5 @@ Treat performance scripts as production code:
|
|
|
219
210
|
- [ ] Single VU smoke test passes cleanly
|
|
220
211
|
- [ ] Script loads test data from external source (CSV, API)
|
|
221
212
|
- [ ] Environment-specific values in config/env vars
|
|
213
|
+
|
|
214
|
+
> **See also:** Full correlation reference in `correlation.md`. Parameterization and data feeds in `test-data.md`. Tool-specific syntax in `../tools/k6.md`, `../tools/jmeter.md`, `../tools/gatling.md`, `../tools/locust.md`.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# SLO & Capacity Planning
|
|
2
|
+
|
|
3
|
+
> Scope: turning load-test results into service level objectives, error budgets, and capacity decisions. Complements `workload-design.md` (designing the load) and `results-analysis.md` (reading the numbers). Read those first if you have not designed or run the test yet.
|
|
4
|
+
|
|
5
|
+
This topic covers the *decision* layer of performance engineering: how to define SLIs/SLOs that mean something, how to convert a load-test curve into a capacity number and a headroom plan, and how to enforce all of it as an automated CI gate.
|
|
6
|
+
|
|
7
|
+
> For LLM-specific SLOs (TTFT, goodput), see `llm-inference.md`. For the tool syntax that enforces SLOs (`ensure`, `thresholds`), see the relevant tool file.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## SLI / SLO / Error Budget
|
|
12
|
+
|
|
13
|
+
### Definitions
|
|
14
|
+
- **SLI (Service Level Indicator)** - the actual measured signal, e.g. request p95 latency, error rate, availability.
|
|
15
|
+
- **SLO (Service Level Objective)** - the target you commit to for an SLI over a window, e.g. "p95 < 500ms for 99% of requests over 28 days."
|
|
16
|
+
- **Error Budget** - `1 - SLO`. A 99% SLO means a 1% error budget: you may violate the objective 1% of the time before it's a breach. Budgets make trade-offs explicit (ship features vs. burn reliability).
|
|
17
|
+
|
|
18
|
+
### Choosing SLIs
|
|
19
|
+
Pick SLIs users actually feel, not ones that are easy to measure:
|
|
20
|
+
|
|
21
|
+
| User-facing concern | SLI | Typical SLO |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Responsiveness | Request p95 latency | < 500ms |
|
|
24
|
+
| Worst-case tail | Request p99 latency | < 1,500ms |
|
|
25
|
+
| Reliability | Error rate (5xx + timeouts) | < 1% (99% success) |
|
|
26
|
+
| Availability | Successfully served requests / total | 99.9% |
|
|
27
|
+
| Throughput | Sustained RPS at SLO latency | ≥ peak + headroom |
|
|
28
|
+
| Freshness (data) | Staleness of served data | < 60s |
|
|
29
|
+
|
|
30
|
+
For streaming/LLM, use token-aware SLIs (TTFT, ITL) and **goodput** (fraction of requests meeting all thresholds) instead of raw latency - see `llm-inference.md`.
|
|
31
|
+
|
|
32
|
+
### Windowing
|
|
33
|
+
- Use a **rolling window** (e.g. 28 days) so a single bad day does not trigger an alert; it consumes budget gradually.
|
|
34
|
+
- Report SLO attainment as `good_events / total_events` over the window. If attainment < SLO, the budget is exhausted.
|
|
35
|
+
- Set **alerts on burn rate**, not on instantaneous SLO pass/fail. Fast burn (e.g. 14× rate) means a major incident; slow burn means a trend to watch.
|
|
36
|
+
|
|
37
|
+
### Multi-window burn-rate alerting (pattern)
|
|
38
|
+
Define fast and slow burn signals so you catch both outages and slow erosion:
|
|
39
|
+
```
|
|
40
|
+
Fast burn: burn rate ≥ 14 over 1h → page
|
|
41
|
+
Slow burn: burn rate ≥ 2 over 6h → ticket
|
|
42
|
+
```
|
|
43
|
+
A burn rate of `X` means the budget is being consumed `X` times faster than the 28-day baseline allows.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## From Load Test to Capacity
|
|
48
|
+
|
|
49
|
+
A load test produces a curve of latency/error vs load. Capacity is the point on that curve where SLOs still hold, **plus** the headroom you keep for safety.
|
|
50
|
+
|
|
51
|
+
### The saturation curve
|
|
52
|
+
```
|
|
53
|
+
latency
|
|
54
|
+
│ ╱‾‾‾‾‾‾‾ ← cliff: SLO breach
|
|
55
|
+
│ ─────────────
|
|
56
|
+
└──────────────────────── load (RPS/VUs)
|
|
57
|
+
[safe zone] [capacity point]
|
|
58
|
+
```
|
|
59
|
+
- **Capacity point** = highest sustained load at which all SLOs (p95, error rate, goodput) still pass.
|
|
60
|
+
- **Saturation point** = where throughput flatlines and latency explodes (the "cliff" in `results-analysis.md`).
|
|
61
|
+
- **Never plan to capacity.** Plan to capacity minus headroom.
|
|
62
|
+
|
|
63
|
+
### Headroom rule
|
|
64
|
+
- Target **peak production load at ~60-70% of measured capacity** under normal operation.
|
|
65
|
+
- Keep **~30-40% headroom** to absorb traffic spikes, node failures, and deployment blips without breaching SLO.
|
|
66
|
+
- For autoscaling systems, headroom is what lets new replicas come up before the SLO burns.
|
|
67
|
+
|
|
68
|
+
### Inverting Little's Law to size capacity
|
|
69
|
+
Given a target arrival rate and response time, concurrency is `N = λ × W` (see `workload-design.md`). Use it both ways:
|
|
70
|
+
- **To size load**: known λ and W → required VUs (already in workload-design).
|
|
71
|
+
- **To size infrastructure**: if the test shows a single replica sustains `C` RPS at SLO, replicas needed = `target_RPS / (C × utilization_target)`. With a 70% utilization target and 1,000 target RPS at 200 RPS/replica → `1000 / (200 × 0.7)` ≈ **8 replicas** (round up, add 1 for failure tolerance).
|
|
72
|
+
|
|
73
|
+
### Capacity from the goodput cliff
|
|
74
|
+
For LLM and streaming services, capacity = load at the goodput cliff (see `llm-inference.md`). Report "sustains 200 concurrent users at 99% goodput; goodput drops to 80% at 350" - that 200 is your planning number, 350 is your hard ceiling.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Worked Example
|
|
79
|
+
|
|
80
|
+
**Inputs:** Peak prod = 800 RPS. Test shows p95 < 500ms and error < 1% hold up to 1,200 RPS on the current 6-replica cluster; saturation at ~1,500 RPS.
|
|
81
|
+
|
|
82
|
+
| Decision | Calculation | Result |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| Capacity point | measured SLO-holding load | 1,200 RPS |
|
|
85
|
+
| Planning target | 60-70% of capacity | 720-840 RPS |
|
|
86
|
+
| Headroom | 1,200 - 800 peak | 33% (acceptable) |
|
|
87
|
+
| Replicas at growth | if peak grows to 1,500 RPS: `1500 / (200 × 0.7)` | 11 replicas (was 6) |
|
|
88
|
+
| Alert threshold | 80% of capacity as early warning | alert at ~960 RPS |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## CI Regression Gating
|
|
93
|
+
|
|
94
|
+
SLOs are useless if they are only in a report. Enforce them automatically:
|
|
95
|
+
|
|
96
|
+
1. **Gate on thresholds, not on "it ran."** Every tool has an SLO mechanism - k6 `thresholds`, Artillery `ensure`, Gatling Enterprise assertions, JMeter exit codes. A run with no gate always passes CI (see each tool file's Common Mistakes).
|
|
97
|
+
2. **Compare against baseline, not just absolute.** A p95 of 480ms passes a 500ms SLO but is a +60% regression vs last week's 300ms. Track percentile deltas build-over-build.
|
|
98
|
+
3. **Set a regression threshold.** e.g. "fail the build if p95 rises > 10% vs baseline, or error rate > 0.5%." Avoid zero-tolerance (noise); avoid loose (misses real regressions).
|
|
99
|
+
4. **Require statistical significance for soak/large runs.** A 1-run blip should not fail CI; use multiple iterations or a confidence band.
|
|
100
|
+
5. **Publish the SLO report in the pipeline.** Surface p50/p95/p99, throughput, error rate, and budget consumption on every run so regressions are visible, not buried.
|
|
101
|
+
|
|
102
|
+
> For the per-tool gate syntax, see `../tools/k6.md` (`thresholds`), `../tools/artillery.md` (`ensure`), `../tools/gatling.md`, `../tools/jmeter.md`. For running these in pipelines, see `test-execution.md`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Common Mistakes
|
|
107
|
+
|
|
108
|
+
- **Averaging for SLOs** - an average of 200ms can hide a p99 of 5s. SLOs must be percentile-based.
|
|
109
|
+
- **SLO with no error budget** - a target with no burn policy becomes a meaningless number; you can never "spend" reliability intentionally.
|
|
110
|
+
- **Planning to the saturation point** - sizing for max measured throughput leaves zero margin; the first spike breaches SLO.
|
|
111
|
+
- **Confusing capacity with peak** - if peak ≈ capacity, you have no headroom. Target 60-70%.
|
|
112
|
+
- **One-shot capacity number** - capacity drifts as code, data, and dependencies change. Re-baseline on major releases.
|
|
113
|
+
- **Gating CI on run success only** - "test passed" ≠ "SLO met." Without a threshold/ensure gate, regressions ship green.
|
|
114
|
+
- **Ignoring tail latency in capacity** - a system can hold p95 at capacity while p99 is 10× worse; gate on the tail users actually feel.
|
|
115
|
+
- **No burn-rate alerting** - alerting on instantaneous SLO state misses slow erosion until the budget is already gone.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Reporting SLO & Capacity
|
|
120
|
+
|
|
121
|
+
Structure the decision section for stakeholders:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
1. SLO SUMMARY
|
|
125
|
+
- p95 < 500ms: 99.2% attainment (budget: 1% used 0.8%)
|
|
126
|
+
- Error rate < 1%: 99.97% attainment
|
|
127
|
+
2. CAPACITY
|
|
128
|
+
- Measured capacity: 1,200 RPS @ SLO
|
|
129
|
+
- Current peak: 800 RPS (33% headroom)
|
|
130
|
+
- Saturation: ~1,500 RPS
|
|
131
|
+
3. RECOMMENDATION
|
|
132
|
+
- Scale to 8 replicas before peak season (growth to 1,500 RPS)
|
|
133
|
+
- Alert at 960 RPS (80% of capacity)
|
|
134
|
+
4. REGRESSION GATE
|
|
135
|
+
- p95 > +10% vs baseline fails CI
|
|
136
|
+
```
|
|
@@ -194,3 +194,5 @@ const user = users[__VU - 1]; // VU 1 gets row 0, VU 2 gets row 1, etc.
|
|
|
194
194
|
- [ ] CSV files available on all injector nodes (distributed tests)
|
|
195
195
|
- [ ] Sensitive data masked/anonymized (not using real PII in test data)
|
|
196
196
|
- [ ] Date/time sensitive records accounted for (e.g., expiry dates set far in future)
|
|
197
|
+
|
|
198
|
+
> **See also:** Tool-specific parameterization syntax in `../tools/k6.md` (SharedArray), `../tools/jmeter.md` (CSV Data Set Config), `../tools/gatling.md` (feeders), `../tools/locust.md`. Distributing data to injectors in `test-execution.md`.
|
|
@@ -114,12 +114,37 @@ bzt tests/load-test.jmx \
|
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
### AWS (DIY Cloud Execution)
|
|
117
|
-
```bash
|
|
118
|
-
# Spin up EC2 injectors via Terraform, then run JMeter/k6
|
|
119
|
-
# Use Auto Scaling Groups for burst capacity
|
|
120
|
-
# Use S3 to store results, CloudWatch for metrics
|
|
121
117
|
|
|
122
|
-
|
|
118
|
+
```bash
|
|
119
|
+
# 1. Launch injectors with AWS CLI (or Terraform)
|
|
120
|
+
aws ec2 run-instances \
|
|
121
|
+
--image-id ami-0abcdef1234567890 \
|
|
122
|
+
--instance-type c5.2xlarge \
|
|
123
|
+
--count 4 \
|
|
124
|
+
--spot-price "0.15" \
|
|
125
|
+
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=load-injector}]'
|
|
126
|
+
|
|
127
|
+
# 2. Install k6 on each injector (via user-data or SSM)
|
|
128
|
+
aws ssm send-command \
|
|
129
|
+
--instance-ids i-0abc i-0def i-0ghi i-0jkl \
|
|
130
|
+
--document-name "AWS-RunShellScript" \
|
|
131
|
+
--parameters 'commands=["sudo apt-get install -y k6"]'
|
|
132
|
+
|
|
133
|
+
# 3. Distribute the test script and run in parallel
|
|
134
|
+
for ip in 10.0.1.{10..13}; do
|
|
135
|
+
scp script.js ec2-user@$ip:~/ && \
|
|
136
|
+
ssh ec2-user@$ip "k6 run --vus 250 --duration 10m script.js --out json=/tmp/results.json" &
|
|
137
|
+
done
|
|
138
|
+
wait
|
|
139
|
+
|
|
140
|
+
# 4. Collect results to S3
|
|
141
|
+
for ip in 10.0.1.{10..13}; do
|
|
142
|
+
scp ec2-user@$ip:/tmp/results.json ./results/$(echo $ip | tr '.' '-')-results.json
|
|
143
|
+
done
|
|
144
|
+
aws s3 cp ./results/ s3://my-load-test-bucket/runs/$(date +%Y%m%d)/ --recursive
|
|
145
|
+
|
|
146
|
+
# Cost optimization: use Spot instances for injectors (60-90% cheaper).
|
|
147
|
+
# Handle interruptions with a 2-min warning hook that flushes partial results.
|
|
123
148
|
```
|
|
124
149
|
|
|
125
150
|
---
|
|
@@ -279,3 +304,5 @@ Always monitor the injector resources alongside the SUT:
|
|
|
279
304
|
- [ ] Smoke test (1 VU) passed before full run
|
|
280
305
|
- [ ] Team notified of test window
|
|
281
306
|
- [ ] Rollback plan in place (especially for production testing)
|
|
307
|
+
|
|
308
|
+
> **See also:** Tool-specific CLI and distributed setup in `../tools/jmeter.md`, `../tools/k6.md`, `../tools/locust.md`, `../tools/gatling.md`, `../tools/artillery.md`. Monitoring during execution in `observability.md`. Production safety controls in `production-testing.md`.
|