quic_performance_benchmark_test.go RawHistoryFolder
1// This file holds the documentable QUIC performance benchmarks. They reuse the
2// harness in perf_evidence_test.go: the real internal/adapters/quic transport
3// behind a [quicnettest.Proxy] and a real sessionwire conversation whose
4// representative exchange is "Input -> valid Output -> Ack".
5//
6// Each benchmark reports latency percentiles (p50/p95/p99/max) in ns/op,
7// allocation counters via -benchmem, process CPU per exchange (getrusage),
8// proxy-carried datagram bytes per exchange, retained heap, and goroutines. Run
9// them isolated so the process-wide CPU figure stays meaningful:
10//
11// go test ./internal/adapters/quicnettest -run '^$' -bench BenchmarkRealQUICExchange -benchmem -benchtime 50x
12// go test ./internal/adapters/quicnettest -run '^$' -bench BenchmarkRealQUICBlackoutRecovery -benchtime 10x
13package quicnettest_test
14
15import (
16 "runtime"
17 "testing"
18 "time"
19
20 "github.com/bnema/vev/internal/adapters/quicnettest"
21 "github.com/bnema/vev/internal/ports"
22)
23
24// BenchmarkRealQUICExchange measures the representative typed exchange for both
25// representative profiles.
26func BenchmarkRealQUICExchange(b *testing.B) {
27 for _, profile := range perfProfiles() {
28 b.Run(profile.name, func(b *testing.B) {
29 benchmarkPerfExchange(b, profile.link)
30 })
31 }
32}
33
34func benchmarkPerfExchange(b *testing.B, link quicnettest.LinkConfig) {
35 if testing.Short() {
36 b.Skip("real QUIC benchmark is skipped in short mode")
37 }
38 baselineGoroutines := runtime.NumGoroutine()
39 fx := startPerfFixture(b, link)
40 client, server := startPerfConversation(b, fx)
41 if _, err := perfExchange(client, server, 1); err != nil {
42 b.Fatalf("warmup exchange: %v", err)
43 }
44
45 latencies := make([]time.Duration, 0, b.N)
46 var appBytes uint64
47 wireBefore := fx.wireBytes()
48 cpuBefore := readPerfCPU()
49 b.ReportAllocs()
50 for i := 0; b.Loop(); i++ {
51 duration, err := perfExchange(client, server, uint64(1000+i))
52 if err != nil {
53 b.Fatalf("exchange %d: %v", i, err)
54 }
55 latencies = append(latencies, duration)
56 appBytes += perfInputPayload + perfOutputPayload
57 }
58 cpuAfter := readPerfCPU()
59 wireAfter := fx.wireBytes()
60
61 runtime.GC()
62 retained := readPerfMem()
63
64 reportPerfBenchmarkMetrics(b, latencies, cpuAfter.total()-cpuBefore.total(), wireAfter-wireBefore, appBytes)
65 b.ReportMetric(float64(retained.heapAlloc), "heap-bytes")
66 b.ReportMetric(float64(retained.goroutines), "goroutines")
67
68 perfBoundedCleanup(b, fx, client, server, baselineGoroutines)
69}
70
71// BenchmarkRealQUICBlackoutRecovery measures the latency from a blackout
72// clearing until an exchange that was in flight during the blackout completes,
73// on the train profile. Run with a small -benchtime; each iteration waits for
74// perfBlackoutWindow plus QUIC recovery.
75func BenchmarkRealQUICBlackoutRecovery(b *testing.B) {
76 benchmarkPerfRecovery(b, perfBlackoutRecoveryOnce)
77}
78
79// BenchmarkRealQUICNATRebindRecovery measures the latency from a proxy NAT
80// rebinding until the next exchange completes, on the train profile.
81func BenchmarkRealQUICNATRebindRecovery(b *testing.B) {
82 benchmarkPerfRecovery(b, perfRebindRecoveryOnce)
83}
84
85func benchmarkPerfRecovery(b *testing.B, recoverOnce func(*perfFixture, ports.ClientConnection, ports.ServerConnection, uint64) (time.Duration, error)) {
86 if testing.Short() {
87 b.Skip("real QUIC recovery benchmark is skipped in short mode")
88 }
89 baselineGoroutines := runtime.NumGoroutine()
90 fx := startPerfFixture(b, trainProfile())
91 client, server := startPerfConversation(b, fx)
92 if _, err := perfExchange(client, server, 1); err != nil {
93 b.Fatalf("warmup exchange: %v", err)
94 }
95
96 latencies := make([]time.Duration, 0, b.N)
97 b.ReportAllocs()
98 for i := 0; b.Loop(); i++ {
99 duration, err := recoverOnce(fx, client, server, uint64(1000+i))
100 if err != nil {
101 b.Fatalf("recovery %d: %v", i, err)
102 }
103 latencies = append(latencies, duration)
104 }
105 reportPerfBenchmarkMetrics(b, latencies, 0, 0, 0)
106
107 perfBoundedCleanup(b, fx, client, server, baselineGoroutines)
108}
109
110// reportPerfBenchmarkMetrics emits the custom per-op metrics shared by every
111// benchmark in this file. cpuTotal, wireBytes, and appBytes are totals for the
112// whole measured loop; a zero total disables that metric, which is what the
113// recovery benchmarks want because their dominant cost is QUIC backoff, not
114// throughput.
115func reportPerfBenchmarkMetrics(b *testing.B, latencies []time.Duration, cpuTotal time.Duration, wireBytes, appBytes uint64) {
116 b.Helper()
117 latency := perfLatencyFor(latencies)
118 b.ReportMetric(float64(latency.P50.Nanoseconds()), "p50-ns/op")
119 b.ReportMetric(float64(latency.P95.Nanoseconds()), "p95-ns/op")
120 b.ReportMetric(float64(latency.P99.Nanoseconds()), "p99-ns/op")
121 b.ReportMetric(float64(latency.Max.Nanoseconds()), "max-ns/op")
122
123 operations := float64(len(latencies))
124 if operations == 0 {
125 return
126 }
127 if cpuTotal > 0 {
128 b.ReportMetric(float64(cpuTotal.Nanoseconds())/operations, "cpu-ns/op")
129 }
130 if wireBytes > 0 {
131 b.ReportMetric(float64(wireBytes)/operations, "wire-bytes/op")
132 }
133 if appBytes > 0 {
134 b.ReportMetric(float64(appBytes)/operations, "app-bytes/op")
135 }
136}