detached fda3c2b

main.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// loadtest replays a traffic scenario against an ermite server and reports
4// throughput and latency per request, while holding open the live updates
5// streams that the pages of real visitors keep.
6//
7//	go run ./scripts/loadtest -url http://127.0.0.1:7418 -scenario home
8//	go run ./scripts/loadtest -scenario repo -var repo=neferwl -rate 500
9//
10// Without -rate it runs closed-loop: -c clients send requests back to back,
11// which measures the maximum throughput. With -rate it runs open-loop: it
12// sends that many requests per second whatever the latency, which measures
13// the latency at a target load; a request that finds no free client is
14// counted as missed. See scripts/loadtest/README.md.
15package main
16
17import (
18	"context"
19	"encoding/json"
20	"flag"
21	"fmt"
22	"math"
23	"os"
24	"os/signal"
25	"strings"
26	"time"
27)
28
29func (c config) validate() error {
30	switch {
31	case c.clients < 1:
32		return fmt.Errorf("-c must be at least 1, got %d", c.clients)
33	case c.duration <= 0:
34		return fmt.Errorf("-d must be positive, got %v", c.duration)
35	case c.warmup < 0:
36		return fmt.Errorf("-warmup must not be negative, got %v", c.warmup)
37	case !(c.streams >= 0) || math.IsInf(c.streams, 0):
38		return fmt.Errorf("-streams must be a finite number >= 0, got %v", c.streams)
39	case !(c.rate >= 0) || math.IsInf(c.rate, 0):
40		return fmt.Errorf("-rate must be a finite number >= 0, got %v", c.rate)
41	case c.rate > 1e6:
42		return fmt.Errorf("-rate is at most 1000000, got %v", c.rate)
43	}
44	return nil
45}
46
47type varsFlag map[string]string
48
49func (v varsFlag) String() string { return fmt.Sprint(map[string]string(v)) }
50
51func (v varsFlag) Set(s string) error {
52	k, val, ok := strings.Cut(s, "=")
53	if !ok || k == "" {
54		return fmt.Errorf("want name=value, got %q", s)
55	}
56	v[k] = val
57	return nil
58}
59
60func main() {
61	cfg := config{vars: varsFlag{}}
62	flag.StringVar(&cfg.base, "url", "http://127.0.0.1:7418", "base URL of the server")
63	flag.StringVar(&cfg.scenario, "scenario", "home", "built-in scenario name, or path to a .json file")
64	flag.Var(cfg.vars, "var", "scenario variable name=value, repeatable")
65	flag.IntVar(&cfg.clients, "c", 32, "concurrent clients")
66	flag.Float64Var(&cfg.rate, "rate", 0, "requests per second (open-loop); 0 runs closed-loop")
67	flag.DurationVar(&cfg.duration, "d", 10*time.Second, "measured duration")
68	flag.DurationVar(&cfg.warmup, "warmup", 2*time.Second, "unmeasured warm-up before the run")
69	flag.Float64Var(&cfg.streams, "streams", 1, "multiplier of the stream counts of the scenario; 0 opens none")
70	flag.StringVar(&cfg.token, "token", "", "bearer token sent with every request (signed-in pages)")
71	flag.BoolVar(&cfg.gzip, "gzip", true, "ask for compressed responses, as browsers do")
72	flag.StringVar(&cfg.jsonOut, "json", "", "also write the report as JSON to this file")
73	flag.Parse()
74
75	if err := cfg.validate(); err != nil {
76		fmt.Fprintln(os.Stderr, "loadtest:", err)
77		os.Exit(2)
78	}
79	sc, err := loadScenario(cfg.scenario, cfg.vars)
80	if err != nil {
81		fmt.Fprintln(os.Stderr, "loadtest:", err)
82		os.Exit(2)
83	}
84	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
85	defer stop()
86
87	rep, err := run(ctx, cfg, sc)
88	if err != nil {
89		fmt.Fprintln(os.Stderr, "loadtest:", err)
90		os.Exit(1)
91	}
92	if err := rep.print(os.Stdout); err != nil {
93		fmt.Fprintln(os.Stderr, "loadtest:", err)
94		os.Exit(1)
95	}
96	if cfg.jsonOut != "" {
97		data, _ := json.MarshalIndent(rep, "", "  ")
98		if err := os.WriteFile(cfg.jsonOut, append(data, '\n'), 0o644); err != nil {
99			fmt.Fprintln(os.Stderr, "loadtest:", err)
100			os.Exit(1)
101		}
102	}
103	if rep.Failed() {
104		os.Exit(1)
105	}
106}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close