feat(go): step 2 — per-phase request breakdown via httptrace
Instruments HTTP requests with net/http/httptrace.ClientTrace to capture timestamps for DNS, TCP connect, TLS handshake, TTFB, and body transfer. TLS row is omitted automatically for plain http:// URLs. Output is an aligned text table with a separator before the Total row. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,11 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -30,14 +33,46 @@ type Result struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Measure performs an HTTP GET to url and returns a Result.
|
||||
// In Step 1 only the Total phase is populated.
|
||||
// Measure performs an HTTP GET to url and returns a Result with all phases
|
||||
// populated via net/http/httptrace.
|
||||
func Measure(url string) Result {
|
||||
r := Result{URL: url}
|
||||
|
||||
start := time.Now()
|
||||
var (
|
||||
dnsStart time.Time
|
||||
dnsDone time.Time
|
||||
connectStart time.Time
|
||||
connectDone time.Time
|
||||
tlsStart time.Time
|
||||
tlsDone time.Time
|
||||
wroteRequest time.Time
|
||||
firstByte time.Time
|
||||
)
|
||||
|
||||
resp, err := http.Get(url) //nolint:noctx
|
||||
trace := &httptrace.ClientTrace{
|
||||
DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() },
|
||||
DNSDone: func(_ httptrace.DNSDoneInfo) { dnsDone = time.Now() },
|
||||
ConnectStart: func(_, _ string) {
|
||||
if connectStart.IsZero() {
|
||||
connectStart = time.Now()
|
||||
}
|
||||
},
|
||||
ConnectDone: func(_, _ string, _ error) { connectDone = time.Now() },
|
||||
TLSHandshakeStart: func() { tlsStart = time.Now() },
|
||||
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { tlsDone = time.Now() },
|
||||
WroteRequest: func(_ httptrace.WroteRequestInfo) { wroteRequest = time.Now() },
|
||||
GotFirstResponseByte: func() { firstByte = time.Now() },
|
||||
}
|
||||
|
||||
ctx := httptrace.WithClientTrace(context.Background(), trace)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
return r
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
r.Total = Phase{Duration: time.Since(start), Present: true}
|
||||
@@ -46,11 +81,30 @@ func Measure(url string) Result {
|
||||
defer resp.Body.Close()
|
||||
|
||||
_, err = io.Copy(io.Discard, resp.Body)
|
||||
end := time.Now()
|
||||
|
||||
r.Total = Phase{Duration: time.Since(start), Present: true}
|
||||
r.StatusCode = resp.StatusCode
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
}
|
||||
|
||||
r.Total = Phase{Duration: end.Sub(start), Present: true}
|
||||
|
||||
if !dnsStart.IsZero() && !dnsDone.IsZero() {
|
||||
r.DNS = Phase{Duration: dnsDone.Sub(dnsStart), Present: true}
|
||||
}
|
||||
if !connectStart.IsZero() && !connectDone.IsZero() {
|
||||
r.Connect = Phase{Duration: connectDone.Sub(connectStart), Present: true}
|
||||
}
|
||||
if !tlsStart.IsZero() && !tlsDone.IsZero() {
|
||||
r.TLS = Phase{Duration: tlsDone.Sub(tlsStart), Present: true}
|
||||
}
|
||||
if !wroteRequest.IsZero() && !firstByte.IsZero() {
|
||||
r.TTFB = Phase{Duration: firstByte.Sub(wroteRequest), Present: true}
|
||||
}
|
||||
if !firstByte.IsZero() {
|
||||
r.Transfer = Phase{Duration: end.Sub(firstByte), Present: true}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
39
go/main.go
39
go/main.go
@@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"latprobe/internal/probe"
|
||||
)
|
||||
@@ -44,17 +44,48 @@ func main() {
|
||||
_ = count
|
||||
|
||||
failed := false
|
||||
for _, url := range urls {
|
||||
for i, url := range urls {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
r := probe.Measure(url)
|
||||
if r.Err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error %s: %v\n", url, r.Err)
|
||||
fmt.Fprintf(os.Stderr, "error %s: %v\n", r.URL, r.Err)
|
||||
failed = true
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%-45s %d %v\n", r.URL, r.StatusCode, r.Total.Duration.Round(time.Millisecond))
|
||||
printResult(r)
|
||||
}
|
||||
|
||||
if failed {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printResult(r probe.Result) {
|
||||
fmt.Printf("%s (%d)\n", r.URL, r.StatusCode)
|
||||
|
||||
phases := []struct {
|
||||
label string
|
||||
p probe.Phase
|
||||
}{
|
||||
{"DNS lookup ", r.DNS},
|
||||
{"TCP connect ", r.Connect},
|
||||
{"TLS handshake ", r.TLS},
|
||||
{"Server (TTFB) ", r.TTFB},
|
||||
{"Transfer ", r.Transfer},
|
||||
}
|
||||
|
||||
for _, ph := range phases {
|
||||
if ph.p.Present {
|
||||
fmt.Printf(" %s : %8.2f ms\n", ph.label, msec(ph.p))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(" " + strings.Repeat("─", 29))
|
||||
fmt.Printf(" %s : %8.2f ms\n", "Total ", msec(r.Total))
|
||||
}
|
||||
|
||||
func msec(p probe.Phase) float64 {
|
||||
return float64(p.Duration.Microseconds()) / 1000
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user