| 1 | package bakeoff |
| 2 | |
| 3 | // A sweep doubles the concurrent connections (1, 2, 4, …) and runs one |
| 4 | // short load step at each, to find where an app starts to slow down. |
| 5 | |
| 6 | // flatGain is the throughput gain below which doubling the load counts |
| 7 | // as no real gain: extra connections only add waiting. |
| 8 | const flatGain = 1.10 |
| 9 | |
| 10 | // maxErrorRate stops a sweep: past it, the numbers describe failures. |
| 11 | const maxErrorRate = 0.01 |
| 12 | |
| 13 | // KneeResult says where a sweep stopped scaling. |
| 14 | type KneeResult struct { |
| 15 | Found bool // false: still scaling at MaxTested |
| 16 | Concurrency int // the last level that still scaled |
| 17 | PeakRPS float64 // the most requests per second any level reached |
| 18 | P50Ms float64 // median latency at Concurrency |
| 19 | MaxTested int |
| 20 | ErrorsAt int // the first level with more than 1% errors, if any |
| 21 | // ErrorsStatus is the commonest failing status at ErrorsAt: "503" when |
| 22 | // Carloku's edge turned requests away, "502" when the app didn't answer. |
| 23 | ErrorsStatus string |
| 24 | } |
| 25 | |
| 26 | // Knee finds where the app slows down: the fewest connections from which |
| 27 | // no further load in the sweep raised throughput by 10% or more. A single |
| 28 | // noisy step can't end the search early, because later growth resets it. |
| 29 | // A step with more than 1% errors ends the sweep: the app failed there |
| 30 | // rather than slowed, so the last clean step is where it stopped coping, |
| 31 | // and peak throughput counts clean steps only. |
| 32 | func Knee(sweep []LoadResult) KneeResult { |
| 33 | var k KneeResult |
| 34 | recorded := false |
| 35 | for _, r := range sweep { |
| 36 | recorded = recorded || len(r.Status) > 0 |
| 37 | } |
| 38 | for i, r := range sweep { |
| 39 | // A step with no responses at all, in a sweep that records them, |
| 40 | // stalled: nothing completed. (Older results stored oha's rate from |
| 41 | // aborted requests and no errors.) |
| 42 | if recorded && len(r.Status) == 0 { |
| 43 | k.ErrorsAt, k.ErrorsStatus = r.Concurrency, "stalled" |
| 44 | sweep = sweep[:i] |
| 45 | break |
| 46 | } |
| 47 | if r.ErrorRate > maxErrorRate { |
| 48 | k.ErrorsAt = r.Concurrency |
| 49 | n := 0 |
| 50 | for code, c := range r.Status { |
| 51 | if code != "200" && code != "303" && c > n { |
| 52 | k.ErrorsStatus, n = code, c |
| 53 | } |
| 54 | } |
| 55 | sweep = sweep[:i] |
| 56 | break |
| 57 | } |
| 58 | } |
| 59 | for _, r := range sweep { |
| 60 | if r.RPS > k.PeakRPS { |
| 61 | k.PeakRPS = r.RPS |
| 62 | } |
| 63 | k.MaxTested = r.Concurrency |
| 64 | } |
| 65 | for i, r := range sweep { |
| 66 | if i == len(sweep)-1 { |
| 67 | break |
| 68 | } |
| 69 | gains := false |
| 70 | for _, later := range sweep[i+1:] { |
| 71 | if later.RPS >= r.RPS*flatGain { |
| 72 | gains = true |
| 73 | break |
| 74 | } |
| 75 | } |
| 76 | if !gains { |
| 77 | k.Found, k.Concurrency, k.P50Ms = true, r.Concurrency, r.P50Ms |
| 78 | break |
| 79 | } |
| 80 | } |
| 81 | if !k.Found && k.ErrorsAt > 0 && len(sweep) > 0 { |
| 82 | last := sweep[len(sweep)-1] |
| 83 | k.Found, k.Concurrency, k.P50Ms = true, last.Concurrency, last.P50Ms |
| 84 | } |
| 85 | return k |
| 86 | } |
| 87 | |
| 88 | // SweepShouldStop is true once the last step had too many errors, or the |
| 89 | // last two doublings were both flat. |
| 90 | func SweepShouldStop(sweep []LoadResult) bool { |
| 91 | n := len(sweep) |
| 92 | if n == 0 { |
| 93 | return false |
| 94 | } |
| 95 | if sweep[n-1].ErrorRate > maxErrorRate { |
| 96 | return true |
| 97 | } |
| 98 | return n >= 3 && sweep[n-1].RPS < sweep[n-2].RPS*flatGain && sweep[n-2].RPS < sweep[n-3].RPS*flatGain |
| 99 | } |
| 100 | |