agones.dev/agones@v1.53.0/test/e2e/framework/perf.go (about) 1 package framework 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "os" 8 "path/filepath" 9 "sync" 10 "time" 11 12 "fortio.org/fortio/fhttp" 13 "fortio.org/fortio/stats" 14 "github.com/sirupsen/logrus" 15 16 k8serrors "k8s.io/apimachinery/pkg/api/errors" 17 ) 18 19 // StatsCollector collects latency and throughput counters. 20 // The ReportDuration() method is safe for concurrent use by multiple goroutines. 21 type StatsCollector struct { 22 name string 23 outputDir string 24 version string 25 26 mu sync.Mutex 27 samples []time.Duration 28 statusCounts map[int]int64 29 firstSampleTime time.Time 30 lastSampleTime time.Time 31 } 32 33 // ReportDuration adds a single time measurement. 34 func (p *StatsCollector) ReportDuration(d time.Duration, err error) { 35 p.mu.Lock() 36 defer p.mu.Unlock() 37 38 n := time.Now() 39 if len(p.samples) == 0 { 40 p.firstSampleTime = n 41 } 42 p.lastSampleTime = n 43 p.samples = append(p.samples, d) 44 if p.statusCounts == nil { 45 p.statusCounts = map[int]int64{} 46 } 47 p.statusCounts[errToHTTPStatusCode(err)]++ 48 } 49 50 func errToHTTPStatusCode(err error) int { 51 // crude translation from 'err' to HTTP status code. 52 switch { 53 case err == nil: 54 return http.StatusOK 55 case k8serrors.IsNotFound(err): 56 return http.StatusNotFound 57 case k8serrors.IsConflict(err): 58 return http.StatusConflict 59 case k8serrors.IsUnauthorized(err): 60 return http.StatusUnauthorized 61 case k8serrors.IsServiceUnavailable(err): 62 return http.StatusServiceUnavailable 63 default: 64 return http.StatusInternalServerError 65 } 66 } 67 68 // Report outputs performance report to log. 69 func (p *StatsCollector) Report() { 70 if len(p.samples) == 0 { 71 return 72 } 73 74 h := stats.NewHistogram(0, 1) 75 for _, s := range p.samples { 76 h.Record(s.Seconds()) 77 } 78 79 var rr fhttp.HTTPRunnerResults 80 rr.RunType = "HTTP" 81 rr.Labels = fmt.Sprintf("Agones %s_%s", p.name, p.version) 82 rr.StartTime = time.Now().UTC() 83 rr.ActualDuration = p.lastSampleTime.Sub(p.firstSampleTime) 84 rr.DurationHistogram = h.Export() 85 rr.DurationHistogram.CalcPercentiles([]float64{50, 90, 95, 99, 99.9}) 86 rr.RetCodes = map[int]int64{} 87 rr.ActualQPS = float64(len(p.samples)) / rr.ActualDuration.Seconds() 88 89 logrus. 90 WithField("avg", rr.DurationHistogram.Avg). 91 WithField("count", rr.DurationHistogram.Count). 92 WithField("min", rr.DurationHistogram.Min). 93 WithField("max", rr.DurationHistogram.Max). 94 WithField("p50", rr.DurationHistogram.CalcPercentile(50)). 95 WithField("p90", rr.DurationHistogram.CalcPercentile(90)). 96 WithField("p95", rr.DurationHistogram.CalcPercentile(95)). 97 WithField("p99", rr.DurationHistogram.CalcPercentile(99)). 98 WithField("p999", rr.DurationHistogram.CalcPercentile(99.9)). 99 WithField("duration", p.lastSampleTime.Sub(p.firstSampleTime).Seconds()). 100 Info(p.name) 101 102 if p.outputDir != "" { 103 err := os.MkdirAll(p.outputDir, 0o755) 104 if err != nil { 105 logrus.WithError(err).Errorf("unable to create a folder: %s", p.outputDir) 106 return 107 } 108 109 fname := filepath.Join(p.outputDir, fmt.Sprintf("%s_%s_%s.json", p.name, p.version, rr.StartTime.Format("2006-01-02_1504"))) 110 f, err := os.Create(fname) 111 if err != nil { 112 logrus.WithError(err).Errorf("unable to create performance log: %s", fname) 113 return 114 } 115 defer func() { 116 if cerr := f.Close(); cerr != nil { 117 logrus.Error(cerr) 118 } 119 }() 120 121 e := json.NewEncoder(f) 122 e.SetIndent("", " ") 123 err = e.Encode(rr) 124 if err != nil { 125 logrus.Error(err) 126 } 127 } 128 }