Skip to content

Commit 32889a3

Browse files
authored
Add load testing utility (#2810)
* Add load testing utility * Add k6 load testing set up * Update scripts * Use localhost URL for loadtesting This should generally be used for testing capacity locally, not in a live distribution. The latter stresses the actual servers and can lead to your IP being blocked. * Indicate load testing from HACKING.md
1 parent d35e392 commit 32889a3

File tree

9 files changed

+257
-0
lines changed

9 files changed

+257
-0
lines changed

HACKING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,19 @@ This will restart the server on filesystem changes.
5959

6060
### Running Tests
6161

62+
#### Unit tests
63+
6264
You can run the unit test suite with:
6365

6466
```bash
6567
make test
6668
```
6769

70+
#### Load tests
71+
72+
See the readme's for running load tests via [k6](./test/load-test/k6/README.md)
73+
or [locust](./test/load-test/locust/README.md).
74+
6875
### Building the Playground
6976

7077
The OCaml Playground is compiled separately from the rest of the server. The generated assets can be found in

test/load-test/k6/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_results/

test/load-test/k6/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# k6 Load Tests
2+
3+
This directory contains a [k6](https://grafana.com/docs/k6/latest/) script for
4+
load testing ocaml.org endpoints.
5+
6+
- The k6 test script is defined in [./script.js](./script.js).
7+
- The runner script [./run.sh](./run.sh) will run `./script.js` with the k6
8+
docker image.
9+
10+
11+
## Running load tests
12+
13+
### Prerequisites
14+
15+
- docker
16+
- k6parser
17+
- xdg-open (MacOS users may need to adjust the runner script)
18+
19+
### Usage
20+
21+
``` sh
22+
./run.sh script.js
23+
```
24+
25+
will run a load test against all defined endpoints using 10 virtual users, and
26+
running for 30 seconds.
27+
28+
You can specify different values for the concurrent users and duration using the
29+
`--vus` and `--duration` flags, respectively. E.g., to test with 100 virtual
30+
users for one minute:
31+
32+
``` sh
33+
./run.sh --vus 100 --duration 1m script.js
34+
```
35+
36+
## Reviewing the results
37+
38+
k6 will print out the a summary of results when the test is finished. See
39+
<https://grafana.com/docs/k6/latest/get-started/results-output/> for
40+
documentation on how to interpret the results.
41+
42+
- Detailed results are written to a gzipped JOSN file
43+
`_results/{day}T{time}-results.gz`.
44+
- A browser will be opened with a visualization plotting general responsiveness,
45+
loaded with the file `_results/{day}T{time}-results.report`.

test/load-test/k6/run.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env sh
2+
3+
# Ignore posix compliance errors in shellcheck
4+
#shellcheck disable=SC3000-SC3061
5+
6+
set -eu
7+
set -o pipefail
8+
9+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
10+
11+
RESULTS_DIR=_results
12+
USER_ID=$(id -u) # ensure we can write to the mounted dir
13+
TIME=$(date +%FT%T)
14+
RESULTS="${RESULTS_DIR}/${TIME}-results.gz"
15+
GRAPH="${RESULTS_DIR}/${TIME}-report.html"
16+
17+
18+
mkdir -p "$RESULTS_DIR"
19+
20+
docker run --rm \
21+
-u "$USER_ID" \
22+
-v "$SCRIPT_DIR":/home/k6 grafana/k6 \
23+
--out json="$RESULTS" \
24+
"$@"
25+
26+
k6parser "$RESULTS" --output "$GRAPH"
27+
28+
xdg-open "$GRAPH"

test/load-test/k6/script.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import http from "k6/http";
2+
import { sleep } from "k6";
3+
4+
export const options = {
5+
// A number specifying the number of VUs to run concurrently.
6+
vus: 10,
7+
// A string specifying the total duration of the test run.
8+
duration: "30s",
9+
10+
// The following section contains configuration options for execution of this
11+
// test script in Grafana Cloud.
12+
//
13+
// See https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/
14+
// to learn about authoring and running k6 test scripts in Grafana k6 Cloud.
15+
//
16+
// cloud: {
17+
// // The ID of the project to which the test is assigned in the k6 Cloud UI.
18+
// // By default tests are executed in default project.
19+
// projectID: "",
20+
// // The name of the test in the k6 Cloud UI.
21+
// // Test runs with the same name will be grouped.
22+
// name: "script.js"
23+
// },
24+
25+
// Uncomment this section to enable the use of Browser API in your tests.
26+
//
27+
// See https://grafana.com/docs/k6/latest/using-k6-browser/running-browser-tests/ to learn more
28+
// about using Browser API in your test scripts.
29+
//
30+
// scenarios: {
31+
// // The scenario name appears in the result summary, tags, and so on.
32+
// // You can give the scenario any name, as long as each name in the script is unique.
33+
// ui: {
34+
// // Executor is a mandatory parameter for browser-based tests.
35+
// // Shared iterations in this case tells k6 to reuse VUs to execute iterations.
36+
// //
37+
// // See https://grafana.com/docs/k6/latest/using-k6/scenarios/executors/ for other executor types.
38+
// executor: 'shared-iterations',
39+
// options: {
40+
// browser: {
41+
// // This is a mandatory parameter that instructs k6 to launch and
42+
// // connect to a chromium-based browser, and use it to run UI-based
43+
// // tests.
44+
// type: 'chromium',
45+
// },
46+
// },
47+
// },
48+
// }
49+
};
50+
51+
// The function that defines VU logic.
52+
//
53+
// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more
54+
// about authoring k6 scripts.
55+
56+
const base_url = "http://localhost:8080";
57+
58+
function endpoint(u) {
59+
return base_url + u;
60+
}
61+
62+
export default function () {
63+
// Landing page
64+
http.get(endpoint("/"));
65+
66+
// Core doc page
67+
http.get(endpoint("/p/core/latest/doc/index.html"));
68+
69+
// Top level pages
70+
http.get(endpoint("/install"));
71+
http.get(endpoint("/docs/tour-of-ocaml"));
72+
http.get(endpoint("/docs"));
73+
http.get(endpoint("/platform"));
74+
http.get(endpoint("/packages"));
75+
http.get(endpoint("/community"));
76+
http.get(endpoint("/changelog"));
77+
http.get(endpoint("/play"));
78+
http.get(endpoint("/industrial-users"));
79+
http.get(endpoint("/academic-users"));
80+
81+
// some package searches
82+
// Grouping the urls, see https://grafana.com/docs/k6/latest/using-k6/http-requests/#url-grouping
83+
const package_search_tag = { tags: { name: "PacakageSearch" } };
84+
["http", "server", "cli", "core", "eio", "graph"].forEach((q) => {
85+
http.get(endpoint(`/packages/autocomplete?q=${q}`), package_search_tag);
86+
http.get(endpoint(`/packages/search?q=${q}`), package_search_tag);
87+
});
88+
}

test/load-test/locust/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

test/load-test/locust/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Locust Load Tests
2+
3+
This directory contains a [locust](https://locust.io/) script for load testing
4+
ocaml.org endpoints.
5+
6+
## Running simple load tests
7+
8+
1. Start the test framework running with
9+
10+
``` sh
11+
./main.sh
12+
```
13+
14+
2. Navigate to http://0.0.0.0:8089
15+
16+
3. Configure
17+
- the max number of users to simulate
18+
- the number of new users to add to the simulation every second
19+
- the host (e.g., `http://localhost:8080`)
20+
21+
## Running load tests with multiple cores
22+
23+
``` sh
24+
./main.sh n
25+
```
26+
27+
where `n` is the number of processes to run concurrently.
28+
29+
## Reviewing the load tests
30+
31+
- Click "Stop" when you are finished running your test.
32+
- Review the various tabs, or click "Download data" for options to download the
33+
test results.
34+
- You can also review prometheus metrics about the staging and prod servers at
35+
https://status.ocaml.ci.dev/d/be358r0z9ai9sf/ocaml-org
36+
37+
## Adding new routines
38+
39+
Tests are defined as "tasks" (sequences of site traversal) in
40+
[./locustfile.py](./locustfile.py).

test/load-test/locust/locustfile.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
#
3+
import random
4+
from locust import FastHttpUser, task, between
5+
6+
# obtained via https://random-word-api.herokuapp.com/word?number=200
7+
words = ["sacraria","seedcake","philtered","leadiest","cloverleafs","snaffle","lyrisms","ankhs","bedtimes","carrier","restabling","playlets","occupation","overreport","printers","scintigraphic","spa","corsages","enwound","gossipmonger","hydragog","waterlily","avulsions","sonneteerings","spilt","hemocytes","tamandus","rais","minaret","coliseums","ultramicrotome","attribute","phosphite","scincoids","scooting","frightfulnesses","carbamides","sculpture","irresponsive","overtasks","expertise","weet","consociations","tulles","hared","pigginesses","oversimple","theologs","adverb","inamoratas","teeny","rapacities","assonant","metestrus","cyanohydrin","smiting","polychete","merest","tautological","phyllopod","petahertz","plainspokenness","pavior","penitently","omikrons","cigarlike","foetal","diebacks","downlight","kinship","warmish","titleholders","suppositions","resuscitations","tiffing","outsung","homed","alternated","cranks","piaster","allotters","nonirradiated","protohistories","finned","decouple","shahs","foeman","perfidious","soarers","thoroughpins","gastrulating","thrivers","convention","roughened","uncircumcised","clabbering","leadscrew","panfuls","nilgais","evolver","overvoting","furrower","ichthyosaurs","internalizes","borschts","regrouping","lordlier","roguish","microseismicity","besmiling","mattoids","cholerically","fibrosarcomas","farinha","curricles","triradiate","beringed","electrolysis","kashmirs","dirdums","ignorami","otalgic","lusciously","blotty","pizzaz","educe","pendant","disposable","autolyzes","outjutting","interfused","operagoers","fustian","theretofore","dean","unsullied","goitrogenic","ultrasafe","potboil","geochemistries","outdesigned","ephedras","woodlore","illuminatingly","guardrooms","sheldrakes","leachable","theistically","reconception","beachboys","recriminates","almuds","changeabilities","flareups","machinate","verbalizations","dendrologist","unkept","copulatives","restyles","parceled","caecilians","mortgager","thunderstones","labarums","wiliness","hydroplane","already","unlatch","swineherds","alternate","whodunit","hoodiest","sainted","detract","inspiring","fantastically","macaws","adsorbs","thickets","blogs","greenfields","ariettes","camphor","hornpipe","uninventive","boatyard","boomiest","lollingly","congresspersons","painter","radiocarbons","impiously","unfeigned","matchlocks","screwballs","stickies","muddlers","resentful","meats"]
8+
9+
class OcamlOrgUsere(FastHttpUser):
10+
wait_time = between(1, 5) # range of seconds a user waits between clicks
11+
12+
@task
13+
def landing(self):
14+
self.client.get("/")
15+
self.client.get("/install")
16+
self.client.get("/p/core/latest/doc/index.html")
17+
self.client.get("/docs/tour-of-ocaml")
18+
19+
@task
20+
def top_level_pages(self):
21+
self.client.get("/docs")
22+
self.client.get("/platform")
23+
self.client.get("/packages")
24+
self.client.get("/community")
25+
self.client.get("/changelog")
26+
self.client.get("/play")
27+
self.client.get("/industrial-users")
28+
self.client.get("/academic-users")
29+
30+
@task
31+
def package_searches(self):
32+
query = random.choice(words)
33+
self.client.get(f"/packages/autocomplete?q={query}")
34+
self.client.get(f"/packages/search?q={query}")

test/load-test/locust/main.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env sh
2+
3+
# Ignore posix compliance errors in shellcheck
4+
#shellcheck disable=SC3000-SC3061
5+
6+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
7+
8+
# see https://docs.locust.io/en/stable/running-distributed.html#distributed-load-generation
9+
n_procs="$1"
10+
11+
docker run -p 8089:8089 -v "$SCRIPT_DIR":/mnt/locust locustio/locust \
12+
--locustfile /mnt/locust/locustfile.py \
13+
${n_procs:+"--processes=${n_procs}"} # Build the --processes flag if n_procs is not nil

0 commit comments

Comments
 (0)