package bakeoff import ( "crypto/tls" "io" "net/http" "net/http/httptrace" "sort" "time" ) // Stats summarises a set of samples in milliseconds. type Stats struct { N int P50, P95, P99, Min, Max float64 } func summarise(ms []float64) Stats { sort.Float64s(ms) pick := func(p float64) float64 { return ms[int(p*float64(len(ms)-1)+0.5)] } return Stats{N: len(ms), P50: pick(0.5), P95: pick(0.95), P99: pick(0.99), Min: ms[0], Max: ms[len(ms)-1]} } // timeToFirstByte makes one GET on a fresh connection (DNS, TCP and TLS // included, as a new visitor would pay them) and returns the milliseconds // until the first response byte. func (s *Session) timeToFirstByte(path string) (float64, error) { tr := &http.Transport{DisableKeepAlives: true, TLSClientConfig: &tls.Config{}} defer tr.CloseIdleConnections() c := &http.Client{Transport: tr, Jar: s.Client.Jar, CheckRedirect: s.Client.CheckRedirect, Timeout: s.Client.Timeout} var first time.Time req, _ := http.NewRequest("GET", s.Base.String()+path, nil) req = req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{ GotFirstResponseByte: func() { first = time.Now() }, })) start := time.Now() res, err := c.Do(req) if err != nil { return 0, err } io.Copy(io.Discard, res.Body) res.Body.Close() return float64(first.Sub(start).Microseconds()) / 1000, nil } // MeasureTTFB discards warmup requests, then times n. func MeasureTTFB(s *Session, path string, warmup, n int) (Stats, error) { for i := 0; i < warmup; i++ { if _, err := s.timeToFirstByte(path); err != nil { return Stats{}, err } } ms := make([]float64, 0, n) for i := 0; i < n; i++ { v, err := s.timeToFirstByte(path) if err != nil { return Stats{}, err } ms = append(ms, v) } return summarise(ms), nil }