carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1package bakeoff
2
3import (
4 "encoding/json"
5 "fmt"
6 "os/exec"
7 "strconv"
8 "time"
9)
10
11// LoadResult is one oha run.
12type LoadResult struct {
13 Scenario string
14 Concurrency int
15 RPS, P50Ms, P95Ms, P99Ms float64
16 ErrorRate float64
17 Status map[string]int `json:",omitempty"` // responses by status code
18 Errors map[string]int `json:",omitempty"` // transport errors by message
19}
20
21// ParseOha reads oha's JSON output. Any status other than ok, and any
22// transport error except oha's own end-of-run deadline aborts, is an error.
23func ParseOha(b []byte, ok int) (LoadResult, error) {
24 var o struct {
25 Summary struct {
26 RequestsPerSec float64 `json:"requestsPerSec"`
27 } `json:"summary"`
28 Latency map[string]float64 `json:"latencyPercentiles"`
29 Status map[string]int `json:"statusCodeDistribution"`
30 Errors map[string]int `json:"errorDistribution"`
31 }
32 if err := json.Unmarshal(b, &o); err != nil {
33 return LoadResult{}, err
34 }
35 var good, bad int
36 for code, n := range o.Status {
37 if code == strconv.Itoa(ok) {
38 good += n
39 } else {
40 bad += n
41 }
42 }
43 errs := map[string]int{}
44 for msg, n := range o.Errors {
45 if msg != "aborted due to deadline" {
46 bad += n
47 errs[msg] = n
48 }
49 }
50 r := LoadResult{
51 RPS: o.Summary.RequestsPerSec,
52 P50Ms: o.Latency["p50"] * 1000,
53 P95Ms: o.Latency["p95"] * 1000,
54 P99Ms: o.Latency["p99"] * 1000,
55 Status: o.Status,
56 }
57 if len(errs) > 0 {
58 r.Errors = errs
59 }
60 if good+bad > 0 {
61 r.ErrorRate = float64(bad) / float64(good+bad)
62 } else {
63 // Nothing completed in the whole step: the app stalled. oha still
64 // reports a rate (from requests it aborted at the deadline).
65 r.ErrorRate, r.RPS = 1, 0
66 if n := o.Errors["aborted due to deadline"]; n > 0 {
67 r.Errors = map[string]int{"no response completed (aborted at the deadline)": n}
68 }
69 }
70 return r, nil
71}
72
73// RunLoad drives the app with oha for d at the given concurrency.
74// "read" is GET / (200); "toggle" is POST /todos/<first id>/toggle (303);
75// "read-after" is "read" again, run after the toggle ramp, to show any
76// slow-down left behind by heavy writes.
77func RunLoad(s *Session, scenario string, concurrency int, d time.Duration) (LoadResult, error) {
78 args := []string{"--no-tui", "--output-format", "json", "-z", d.String(), "-c", strconv.Itoa(concurrency),
79 "--redirect", "0", "-H", "Cookie: " + s.CookieHeader(), "-H", "Origin: " + s.origin()}
80 ok := 200
81 target := s.Base.String() + "/"
82 switch scenario {
83 case "read", "read-after":
84 case "toggle":
85 if s.ToggleID == "" {
86 return LoadResult{}, fmt.Errorf("toggle needs a list seeded by Seed")
87 }
88 ok = 303
89 target = s.Base.String() + "/todos/" + s.ToggleID + "/toggle"
90 args = append(args, "-m", "POST", "-T", "application/x-www-form-urlencoded", "-d", "filter=all")
91 default:
92 return LoadResult{}, fmt.Errorf("unknown scenario %q", scenario)
93 }
94 out, err := exec.Command("oha", append(args, target)...).Output()
95 if err != nil {
96 return LoadResult{}, fmt.Errorf("oha: %w", err)
97 }
98 r, err := ParseOha(out, ok)
99 r.Scenario, r.Concurrency = scenario, concurrency
100 return r, err
101}
102