package bakeoff // A sweep doubles the concurrent connections (1, 2, 4, …) and runs one // short load step at each, to find where an app starts to slow down. // flatGain is the throughput gain below which doubling the load counts // as no real gain: extra connections only add waiting. const flatGain = 1.10 // maxErrorRate stops a sweep: past it, the numbers describe failures. const maxErrorRate = 0.01 // KneeResult says where a sweep stopped scaling. type KneeResult struct { Found bool // false: still scaling at MaxTested Concurrency int // the last level that still scaled PeakRPS float64 // the most requests per second any level reached P50Ms float64 // median latency at Concurrency MaxTested int ErrorsAt int // the first level with more than 1% errors, if any // ErrorsStatus is the commonest failing status at ErrorsAt: "503" when // Carloku's edge turned requests away, "502" when the app didn't answer. ErrorsStatus string } // Knee finds where the app slows down: the fewest connections from which // no further load in the sweep raised throughput by 10% or more. A single // noisy step can't end the search early, because later growth resets it. // A step with more than 1% errors ends the sweep: the app failed there // rather than slowed, so the last clean step is where it stopped coping, // and peak throughput counts clean steps only. func Knee(sweep []LoadResult) KneeResult { var k KneeResult recorded := false for _, r := range sweep { recorded = recorded || len(r.Status) > 0 } for i, r := range sweep { // A step with no responses at all, in a sweep that records them, // stalled: nothing completed. (Older results stored oha's rate from // aborted requests and no errors.) if recorded && len(r.Status) == 0 { k.ErrorsAt, k.ErrorsStatus = r.Concurrency, "stalled" sweep = sweep[:i] break } if r.ErrorRate > maxErrorRate { k.ErrorsAt = r.Concurrency n := 0 for code, c := range r.Status { if code != "200" && code != "303" && c > n { k.ErrorsStatus, n = code, c } } sweep = sweep[:i] break } } for _, r := range sweep { if r.RPS > k.PeakRPS { k.PeakRPS = r.RPS } k.MaxTested = r.Concurrency } for i, r := range sweep { if i == len(sweep)-1 { break } gains := false for _, later := range sweep[i+1:] { if later.RPS >= r.RPS*flatGain { gains = true break } } if !gains { k.Found, k.Concurrency, k.P50Ms = true, r.Concurrency, r.P50Ms break } } if !k.Found && k.ErrorsAt > 0 && len(sweep) > 0 { last := sweep[len(sweep)-1] k.Found, k.Concurrency, k.P50Ms = true, last.Concurrency, last.P50Ms } return k } // SweepShouldStop is true once the last step had too many errors, or the // last two doublings were both flat. func SweepShouldStop(sweep []LoadResult) bool { n := len(sweep) if n == 0 { return false } if sweep[n-1].ErrorRate > maxErrorRate { return true } return n >= 3 && sweep[n-1].RPS < sweep[n-2].RPS*flatGain && sweep[n-2].RPS < sweep[n-3].RPS*flatGain }