1// This file holds the QUIC performance-measurement harness and its bounded
2// evidence test. It is deliberately an external test package: it drives the
3// real internal/adapters/quic transport through the adverse-network proxy and a
4// real sessionwire conversation, exactly like quic_integration_test.go, while
5// keeping the simulator package itself free of production dependencies.
6//
7// The harness measures one representative typed exchange, "Input -> valid
8// Output -> Ack", under two profiles:
9//
10// - "clean": the proxy forwards with no impairment,
11// - "train": the deterministic 4G/train profile ([trainProfile]).
12//
13// It reports a reproducible evidence line per profile: per-exchange latency
14// percentiles (p50/p95/p99/max), process CPU per exchange (getrusage
15// RUSAGE_SELF), allocations and bytes per exchange, GC count, retained heap,
16// goroutine count, proxy-carried bytes per exchange, and recovery latency after
17// a blackout and after a NAT rebinding.
18//
19// The test asserts only structural invariants (every exchange round-trips
20// byte-for-byte, percentiles are monotone, the proxy accounting settles, and
21// recovery stays inside a generous liveness ceiling). It deliberately makes no
22// latency-threshold assertion, so it does not become flaky on a loaded machine.
23// Higher-sample measurements live in quic_performance_benchmark_test.go.
24package quicnettest_test
25
26import (
27 "bytes"
28 "context"
29 "encoding/binary"
30 "errors"
31 "fmt"
32 "math"
33 "net"
34 "runtime"
35 "sort"
36 "sync/atomic"
37 "syscall"
38 "testing"
39 "time"
40
41 quicadapter "github.com/bnema/vev/internal/adapters/quic"
42 "github.com/bnema/vev/internal/adapters/quicnettest"
43 "github.com/bnema/vev/internal/adapters/sessionwire"
44 "github.com/bnema/vev/internal/domain"
45 "github.com/bnema/vev/internal/ports"
46 "github.com/bnema/vev/internal/protocol"
47 "github.com/bnema/vev/internal/protocol/wire"
48)
49
50// Bounds keep the evidence test cheap enough to run under -race while still
51// exercising the adverse paths. Callers that want a tighter distribution should
52// use the benchmarks with -benchtime.
53const (
54 perfExchangeTimeout = 25 * time.Second
55 perfRecoveryCeiling = 8 * time.Second
56 perfBlackoutWindow = 100 * time.Millisecond
57 perfBlackoutSamples = 2
58 perfRebindSamples = 2
59 perfWarmupExchanges = 1
60
61 perfInputPayload = 512
62 perfOutputPayload = 512
63 // perfCleanExchanges and perfTrainExchanges cap the measured transcript.
64 // The train profile pays roughly 3 impaired link legs per exchange; the
65 // clean profile is near-free, so it takes the larger sample for a more
66 // meaningful p99. Both keep the bounded test well under a minute -race.
67 perfCleanExchanges = 100
68 perfTrainExchanges = 30
69)
70
71// perfProfile pairs a link profile with the bounded measured transcript size.
72type perfProfile struct {
73 name string
74 link quicnettest.LinkConfig
75 exchanges int
76}
77
78// perfProfiles returns the two representative profiles.
79func perfProfiles() []perfProfile {
80 return []perfProfile{
81 {name: "clean", link: perfCleanProfile(), exchanges: perfCleanExchanges},
82 {name: "train", link: trainProfile(), exchanges: perfTrainExchanges},
83 }
84}
85
86// perfCleanProfile is the unimpaired reference link: the proxy still carries
87// every packet, but adds no latency, loss, duplication, or reordering.
88func perfCleanProfile() quicnettest.LinkConfig { return quicnettest.LinkConfig{} }
89
90// perfFixture is a real QUIC listener behind a [quicnettest.Proxy] plus the
91// atomically accumulated proxy carriage counters used for bytes/work.
92type perfFixture struct {
93 proxy *quicnettest.Proxy
94 listener *quicadapter.Listener
95 fingerprint []byte
96
97 packets atomic.Uint64
98 wireToServer atomic.Uint64
99 wireToClient atomic.Uint64
100}
101
102// wireBytes is the total datagram bytes the proxy ingested in both directions.
103func (f *perfFixture) wireBytes() uint64 {
104 return f.wireToServer.Load() + f.wireToClient.Load()
105}
106
107// startPerfFixture starts the real QUIC listener and proxy, with a fixed seed so
108// the train profile's impairment decisions are reproducible.
109func startPerfFixture(tb testing.TB, link quicnettest.LinkConfig) *perfFixture {
110 tb.Helper()
111 cert, fingerprint, err := quicadapter.GenerateEphemeralCert()
112 if err != nil {
113 tb.Fatalf("generate cert: %v", err)
114 }
115 listener, err := quicadapter.ListenConfig("127.0.0.1:0", cert, quicadapter.Config{}, 4)
116 if err != nil {
117 tb.Fatalf("listen quic: %v", err)
118 }
119 tb.Cleanup(func() { _ = listener.Close() })
120
121 serverAddr, err := net.ResolveUDPAddr("udp", listener.Addr())
122 if err != nil {
123 tb.Fatalf("resolve listener address %q: %v", listener.Addr(), err)
124 }
125 fixture := &perfFixture{listener: listener, fingerprint: fingerprint}
126 proxy, err := quicnettest.New(quicnettest.Config{
127 ServerAddr: serverAddr,
128 Seed: 20260913,
129 QueueCapacity: 2048,
130 ToServer: link,
131 ToClient: link,
132 // The sink only accumulates atomics, so it never blocks a reader.
133 OnPacket: func(event quicnettest.Event) {
134 fixture.packets.Add(1)
135 if event.Direction == quicnettest.ToServer {
136 fixture.wireToServer.Add(uint64(event.Bytes))
137 return
138 }
139 fixture.wireToClient.Add(uint64(event.Bytes))
140 },
141 })
142 if err != nil {
143 tb.Fatalf("new proxy: %v", err)
144 }
145 tb.Cleanup(func() { _ = proxy.Close() })
146 fixture.proxy = proxy
147 return fixture
148}
149
150// perfServerReceive carries one typed server-side receive result.
151type perfServerReceive struct {
152 message protocol.ClientMessage
153 err error
154}
155
156// startPerfConversation dials the real QUIC carriage through the proxy and
157// completes the typed sessionwire preamble (Hello/Welcome).
158func startPerfConversation(tb testing.TB, fx *perfFixture) (ports.ClientConnection, ports.ServerConnection) {
159 tb.Helper()
160 accepted := make(chan struct {
161 transport wire.Transport
162 err error
163 }, 1)
164 go func() {
165 transport, err := fx.listener.Accept()
166 accepted <- struct {
167 transport wire.Transport
168 err error
169 }{transport: transport, err: err}
170 }()
171
172 dialer := quicadapter.DialConfig(fx.proxy.Addr().String(), "vev-bootstrap", fx.fingerprint, quicadapter.Config{}, 15*time.Second)
173 ctx, cancel := context.WithTimeout(context.Background(), perfExchangeTimeout)
174 defer cancel()
175 clientRaw, err := dialer.Dial(ctx)
176 if err != nil {
177 tb.Fatalf("dial through proxy: %v", err)
178 }
179 tb.Cleanup(func() { _ = clientRaw.Close() })
180
181 var serverRaw wire.Transport
182 select {
183 case result := <-accepted:
184 if result.err != nil {
185 tb.Fatalf("accept: %v", result.err)
186 }
187 serverRaw = result.transport
188 case <-time.After(perfExchangeTimeout):
189 tb.Fatalf("timed out waiting for the QUIC listener to accept")
190 }
191 tb.Cleanup(func() { _ = serverRaw.Close() })
192
193 client := sessionwire.NewClientConnection(clientRaw)
194 server := sessionwire.NewServerConnection(serverRaw)
195 tb.Cleanup(func() { _ = client.Close() })
196 tb.Cleanup(func() { _ = server.Close() })
197
198 hello := protocol.Hello{
199 Version: protocol.Version,
200 ClientID: [16]byte{0x70, 0x65, 0x72, 0x66, 0x2d, 0x71, 0x75, 0x69},
201 Name: "perf-quic",
202 Size: domain.Size{Cols: 80, Rows: 24},
203 TermEnv: "xterm-256color",
204 }
205 got, err := perfSendClientMessage(client, server, hello)
206 if err != nil {
207 tb.Fatalf("handshake hello: %v", err)
208 }
209 if gotHello, ok := got.(protocol.Hello); !ok || gotHello.Name != hello.Name || gotHello.Version != hello.Version {
210 tb.Fatalf("server received %#v, want %#v", got, hello)
211 }
212 reply, err := perfSendServerMessage(client, server, protocol.Welcome{SessionID: "perf-quic", SessionName: "perf", Capabilities: 1})
213 if err != nil {
214 tb.Fatalf("handshake welcome: %v", err)
215 }
216 welcome, ok := reply.(protocol.Welcome)
217 if !ok || welcome.SessionID != "perf-quic" {
218 tb.Fatalf("client reply is %#v, want Welcome{SessionID: perf-quic}", reply)
219 }
220 return client, server
221}
222
223// perfSendClientMessage sends one typed client message while the server accepts
224// it, and returns the message the server observed.
225func perfSendClientMessage(client ports.ClientConnection, server ports.ServerConnection, message protocol.ClientMessage) (protocol.ClientMessage, error) {
226 received := make(chan perfServerReceive, 1)
227 go func() {
228 got, err := server.ReceiveClient()
229 received <- perfServerReceive{message: got, err: err}
230 }()
231 if err := client.SendClient(message); err != nil {
232 return nil, fmt.Errorf("send %T: %w", message, err)
233 }
234 select {
235 case result := <-received:
236 if result.err != nil {
237 return nil, fmt.Errorf("receive after %T: %w", message, result.err)
238 }
239 return result.message, nil
240 case <-time.After(perfExchangeTimeout):
241 return nil, fmt.Errorf("timed out receiving after %T", message)
242 }
243}
244
245// perfSendServerMessage sends one typed server message while the client accepts
246// it, and returns the message the client observed.
247func perfSendServerMessage(client ports.ClientConnection, server ports.ServerConnection, message protocol.ServerMessage) (protocol.ServerMessage, error) {
248 sent := make(chan error, 1)
249 go func() { sent <- server.SendServer(message) }()
250 reply, err := client.ReceiveServer()
251 if err != nil {
252 return nil, fmt.Errorf("receive %T: %w", message, err)
253 }
254 if err := <-sent; err != nil {
255 return nil, fmt.Errorf("send %T: %w", message, err)
256 }
257 return reply, nil
258}
259
260// perfExchange runs one representative typed exchange: the client sends an
261// Input, the server answers with a semantically valid Output, and the client
262// acknowledges it. It returns the wall-clock round-trip duration of the whole
263// exchange and any structural error, so it is safe to call from a goroutine.
264func perfExchange(client ports.ClientConnection, server ports.ServerConnection, seq uint64) (time.Duration, error) {
265 start := time.Now()
266 data := perfPayload(seq, perfInputPayload)
267
268 receivedInput, err := perfSendClientMessage(client, server, protocol.Input{InputSeq: seq, ActionID: seq, Data: data})
269 if err != nil {
270 return 0, err
271 }
272 input, ok := receivedInput.(protocol.Input)
273 if !ok {
274 return 0, fmt.Errorf("server received %T, want protocol.Input", receivedInput)
275 }
276 if input.InputSeq != seq || input.ActionID != seq {
277 return 0, fmt.Errorf("server input seq/action = %d/%d, want %d/%d", input.InputSeq, input.ActionID, seq, seq)
278 }
279 if !bytes.Equal(input.Data, data) {
280 return 0, fmt.Errorf("server input %d payload differs from the sent bytes", seq)
281 }
282
283 reply, err := perfSendServerMessage(client, server, perfOutput(seq))
284 if err != nil {
285 return 0, err
286 }
287 output, ok := reply.(protocol.Output)
288 if !ok {
289 return 0, fmt.Errorf("client received %T, want protocol.Output", reply)
290 }
291 if err := protocol.ValidateOutput(output); err != nil {
292 return 0, fmt.Errorf("client output is invalid: %w", err)
293 }
294
295 ack := protocol.Ack{Epoch: output.Epoch, State: output.New}
296 receivedAck, err := perfSendClientMessage(client, server, ack)
297 if err != nil {
298 return 0, err
299 }
300 gotAck, ok := receivedAck.(protocol.Ack)
301 if !ok {
302 return 0, fmt.Errorf("server received %T, want protocol.Ack", receivedAck)
303 }
304 if gotAck != ack {
305 return 0, fmt.Errorf("server ack = %#v, want %#v", gotAck, ack)
306 }
307 return time.Since(start), nil
308}
309
310// perfOutput builds a semantically valid full-screen Output carrying a payload,
311// so the reverse direction exercises real bytes rather than an empty heartbeat.
312func perfOutput(seq uint64) protocol.Output {
313 return protocol.Output{
314 Epoch: 1,
315 Base: 0,
316 New: seq,
317 Full: true,
318 Size: domain.Size{Cols: 80, Rows: 24},
319 Context: perfViewContext(),
320 Data: perfPayload(seq, perfOutputPayload),
321 }
322}
323
324func perfViewContext() *protocol.ViewContext {
325 return &protocol.ViewContext{
326 Publication: 3,
327 Route: protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{
328 LifecycleID: domain.SessionLifecycleID{1},
329 SessionName: "work",
330 }},
331 TabID: "tab-1",
332 FocusedPaneID: "pane-1",
333 }
334}
335
336func perfPayload(seq uint64, size int) []byte {
337 payload := make([]byte, size)
338 binary.BigEndian.PutUint64(payload, seq)
339 for i := 8; i < len(payload); i++ {
340 payload[i] = byte(seq)
341 }
342 return payload
343}
344
345// perfLatency is a nearest-rank latency summary over measured exchanges.
346type perfLatency struct {
347 Count int
348 P50, P95, P99, Max time.Duration
349}
350
351func perfLatencyFor(samples []time.Duration) perfLatency {
352 if len(samples) == 0 {
353 return perfLatency{}
354 }
355 sorted := append([]time.Duration(nil), samples...)
356 sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
357 return perfLatency{
358 Count: len(sorted),
359 P50: perfRank(sorted, 0.50),
360 P95: perfRank(sorted, 0.95),
361 P99: perfRank(sorted, 0.99),
362 Max: sorted[len(sorted)-1],
363 }
364}
365
366// perfRank returns the nearest-rank quantile of a sorted sample.
367func perfRank(sorted []time.Duration, quantile float64) time.Duration {
368 index := int(math.Ceil(quantile*float64(len(sorted)))) - 1
369 if index < 0 {
370 index = 0
371 }
372 if index >= len(sorted) {
373 index = len(sorted) - 1
374 }
375 return sorted[index]
376}
377
378// perfCPUSample is a process CPU snapshot from getrusage(RUSAGE_SELF).
379//
380// getrusage reports process-wide CPU, so the CPU-per-exchange figure is stable
381// when the package is run in isolation (for example with -run or a benchmark
382// filter) rather than alongside other tests.
383type perfCPUSample struct {
384 user time.Duration
385 sys time.Duration
386}
387
388func (s perfCPUSample) total() time.Duration { return s.user + s.sys }
389
390func readPerfCPU() perfCPUSample {
391 var usage syscall.Rusage
392 if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err != nil {
393 return perfCPUSample{}
394 }
395 return perfCPUSample{
396 user: time.Duration(usage.Utime.Sec)*time.Second + time.Duration(usage.Utime.Usec)*time.Microsecond,
397 sys: time.Duration(usage.Stime.Sec)*time.Second + time.Duration(usage.Stime.Usec)*time.Microsecond,
398 }
399}
400
401// perfMemSample captures the runtime allocation counters used for
402// allocations/work, GC count, retained heap, and goroutines.
403type perfMemSample struct {
404 totalAlloc uint64
405 mallocs uint64
406 heapAlloc uint64
407 numGC uint32
408 goroutines int
409}
410
411func readPerfMem() perfMemSample {
412 var stats runtime.MemStats
413 runtime.ReadMemStats(&stats)
414 return perfMemSample{
415 totalAlloc: stats.TotalAlloc,
416 mallocs: stats.Mallocs,
417 heapAlloc: stats.HeapAlloc,
418 numGC: stats.NumGC,
419 goroutines: runtime.NumGoroutine(),
420 }
421}
422
423// perfBlackoutRecoveryOnce runs one recovery sample: an exchange is put in
424// flight, the proxy drops every packet for perfBlackoutWindow, and the returned
425// duration is measured from the moment the blackout clears until that exchange
426// completes.
427func perfBlackoutRecoveryOnce(fx *perfFixture, client ports.ClientConnection, server ports.ServerConnection, seq uint64) (time.Duration, error) {
428 if _, err := perfExchange(client, server, seq); err != nil {
429 return 0, fmt.Errorf("pre-blackout baseline: %w", err)
430 }
431 fx.proxy.SetBlackout(true)
432 result := make(chan error, 1)
433 go func() {
434 _, err := perfExchange(client, server, seq+1_000_000)
435 result <- err
436 }()
437 time.Sleep(perfBlackoutWindow)
438 start := time.Now()
439 fx.proxy.SetBlackout(false)
440 select {
441 case err := <-result:
442 if err != nil {
443 return 0, fmt.Errorf("in-flight exchange after blackout: %w", err)
444 }
445 return time.Since(start), nil
446 case <-time.After(perfRecoveryCeiling):
447 return 0, fmt.Errorf("blackout recovery exceeded %s", perfRecoveryCeiling)
448 }
449}
450
451// perfRebindRecoveryOnce runs one recovery sample: the proxy changes its
452// upstream source address, and the returned duration is measured from the
453// rebinding until the next exchange completes.
454func perfRebindRecoveryOnce(fx *perfFixture, client ports.ClientConnection, server ports.ServerConnection, seq uint64) (time.Duration, error) {
455 if _, err := perfExchange(client, server, seq); err != nil {
456 return 0, fmt.Errorf("pre-rebind baseline: %w", err)
457 }
458 if err := fx.proxy.Rebind(); err != nil {
459 return 0, fmt.Errorf("rebind: %w", err)
460 }
461 start := time.Now()
462 if _, err := perfExchange(client, server, seq+2_000_000); err != nil {
463 return 0, fmt.Errorf("exchange after rebind: %w", err)
464 }
465 return time.Since(start), nil
466}
467
468// perfWaitAccounting polls the proxy counters until every accepted packet is
469// either sent or dropped, then returns the settled snapshot.
470func perfWaitAccounting(tb testing.TB, fx *perfFixture, timeout time.Duration) quicnettest.Stats {
471 tb.Helper()
472 deadline := time.Now().Add(timeout)
473 for {
474 stats := fx.proxy.Stats()
475 if perfAccounted(stats.ToServer) && perfAccounted(stats.ToClient) {
476 return stats
477 }
478 if time.Now().After(deadline) {
479 tb.Fatalf("proxy accounting did not settle within %s: %s", timeout, stats)
480 }
481 time.Sleep(10 * time.Millisecond)
482 }
483}
484
485func perfAccounted(link quicnettest.LinkStats) bool {
486 drops := link.LossDrops + link.BlackoutDrops + link.OverflowDrops
487 return link.Received+link.Duplicates == link.Sent+drops
488}
489
490// perfBoundedCleanup closes every resource with a deadline and requires the
491// goroutine count to return near its baseline.
492func perfBoundedCleanup(tb testing.TB, fx *perfFixture, client ports.ClientConnection, server ports.ServerConnection, baseline int) {
493 tb.Helper()
494 closed := make(chan error, 1)
495 go func() {
496 closed <- errors.Join(client.Close(), server.Close(), fx.listener.Close(), fx.proxy.Close())
497 }()
498 select {
499 case err := <-closed:
500 if err != nil {
501 tb.Fatalf("cleanup: %v", err)
502 }
503 case <-time.After(perfExchangeTimeout):
504 tb.Fatalf("cleanup did not complete within %s", perfExchangeTimeout)
505 }
506 deadline := time.Now().Add(5 * time.Second)
507 const slack = 8
508 for {
509 if runtime.NumGoroutine() <= baseline+slack {
510 return
511 }
512 if time.Now().After(deadline) {
513 tb.Fatalf("goroutines did not drain: baseline %d, still %d", baseline, runtime.NumGoroutine())
514 }
515 time.Sleep(25 * time.Millisecond)
516 }
517}
518
519// TestRealQUICPerformanceEvidence produces one bounded, reproducible evidence
520// record per profile. It asserts only structural invariants; the measured
521// values are emitted with t.Logf and are meant to be read with -v, e.g.
522//
523// go test ./internal/adapters/quicnettest -run TestRealQUICPerformanceEvidence -v
524func TestRealQUICPerformanceEvidence(t *testing.T) {
525 if testing.Short() {
526 t.Skip("real QUIC performance evidence is skipped in short mode")
527 }
528 for _, profile := range perfProfiles() {
529 t.Run(profile.name, func(t *testing.T) {
530 perfEvidenceRun(t, profile)
531 })
532 }
533}
534
535func perfEvidenceRun(t *testing.T, profile perfProfile) {
536 t.Helper()
537 baselineGoroutines := runtime.NumGoroutine()
538 fx := startPerfFixture(t, profile.link)
539
540 handshakeStart := time.Now()
541 client, server := startPerfConversation(t, fx)
542 handshakeLatency := time.Since(handshakeStart)
543
544 for i := 1; i <= perfWarmupExchanges; i++ {
545 if _, err := perfExchange(client, server, uint64(i)); err != nil {
546 t.Fatalf("warmup exchange %d: %v", i, err)
547 }
548 }
549
550 cpuBefore := readPerfCPU()
551 memBefore := readPerfMem()
552 wireBefore := fx.wireBytes()
553 latencies := make([]time.Duration, 0, profile.exchanges)
554 measureStart := time.Now()
555 for i := 1; i <= profile.exchanges; i++ {
556 duration, err := perfExchange(client, server, uint64(1000+i))
557 if err != nil {
558 t.Fatalf("exchange %d: %v", i, err)
559 }
560 latencies = append(latencies, duration)
561 }
562 measureWall := time.Since(measureStart)
563 cpuAfter := readPerfCPU()
564 memAfter := readPerfMem()
565 wireAfter := fx.wireBytes()
566
567 latency := perfLatencyFor(latencies)
568
569 blackouts := make([]time.Duration, 0, perfBlackoutSamples)
570 for i := 0; i < perfBlackoutSamples; i++ {
571 duration, err := perfBlackoutRecoveryOnce(fx, client, server, uint64(2000+i))
572 if err != nil {
573 t.Fatalf("blackout recovery sample %d: %v", i, err)
574 }
575 blackouts = append(blackouts, duration)
576 }
577 rebinds := make([]time.Duration, 0, perfRebindSamples)
578 for i := 0; i < perfRebindSamples; i++ {
579 duration, err := perfRebindRecoveryOnce(fx, client, server, uint64(3000+i))
580 if err != nil {
581 t.Fatalf("rebind recovery sample %d: %v", i, err)
582 }
583 rebinds = append(rebinds, duration)
584 }
585
586 runtime.GC()
587 retained := readPerfMem()
588
589 stats := perfWaitAccounting(t, fx, 10*time.Second)
590
591 blackoutLatency := perfLatencyFor(blackouts)
592 rebindLatency := perfLatencyFor(rebinds)
593 cpuTotal := cpuAfter.total() - cpuBefore.total()
594 cpuPerExchange := cpuTotal / time.Duration(profile.exchanges)
595 allocsPerExchange := float64(memAfter.mallocs-memBefore.mallocs) / float64(profile.exchanges)
596 bytesPerExchange := float64(memAfter.totalAlloc-memBefore.totalAlloc) / float64(profile.exchanges)
597 gcDelta := int64(memAfter.numGC) - int64(memBefore.numGC)
598 appBytesPerExchange := int64(perfInputPayload + perfOutputPayload)
599 wireBytesPerExchange := float64(wireAfter-wireBefore) / float64(profile.exchanges)
600 wireAmplification := wireBytesPerExchange / float64(appBytesPerExchange)
601
602 // Structural invariants only: no latency threshold, so the evidence test
603 // stays valid on a loaded or virtualized host.
604 if latency.Count != profile.exchanges {
605 t.Fatalf("latency sample count = %d, want %d", latency.Count, profile.exchanges)
606 }
607 if !(latency.P50 <= latency.P95 && latency.P95 <= latency.P99 && latency.P99 <= latency.Max) {
608 t.Fatalf("latency percentiles are not monotone: %+v", latency)
609 }
610 for _, duration := range append(append([]time.Duration(nil), blackouts...), rebinds...) {
611 if duration <= 0 || duration > perfRecoveryCeiling {
612 t.Fatalf("recovery latency %s is outside the liveness window (0,%s]", duration, perfRecoveryCeiling)
613 }
614 }
615 if stats.ToServer.DeliveryErrors != 0 || stats.ToClient.DeliveryErrors != 0 {
616 t.Fatalf("proxy delivery failed: %s", stats)
617 }
618 if stats.ToServer.OverflowDrops != 0 || stats.ToClient.OverflowDrops != 0 {
619 t.Fatalf("bounded queue overflowed: %s", stats)
620 }
621 if stats.ToServer.Received == 0 || stats.ToServer.Sent == 0 || stats.ToClient.Sent == 0 {
622 t.Fatalf("proxy carried no traffic: %s", stats)
623 }
624 if stats.ToServer.BlackoutDrops == 0 {
625 t.Fatalf("blackout did not drop packets: %s", stats)
626 }
627 if stats.Rebinds != uint64(perfRebindSamples) {
628 t.Fatalf("rebinds = %d, want %d", stats.Rebinds, perfRebindSamples)
629 }
630
631 t.Logf("PERF profile=%s exchanges=%d handshake=%s measured_wall=%s", profile.name, profile.exchanges, handshakeLatency, measureWall)
632 t.Logf("PERF latency profile=%s p50=%s p95=%s p99=%s max=%s", profile.name, latency.P50, latency.P95, latency.P99, latency.Max)
633 t.Logf("PERF cost profile=%s cpu_total=%s cpu_per_exchange=%s allocs_per_exchange=%.1f bytes_per_exchange=%.0f gc_delta=%d goroutines=%d heap_retained_bytes=%d",
634 profile.name, cpuTotal, cpuPerExchange, allocsPerExchange, bytesPerExchange, gcDelta, retained.goroutines, retained.heapAlloc)
635 t.Logf("PERF bytes profile=%s app_bytes_per_exchange=%d wire_bytes_per_exchange=%.1f wire_amplification=%.2f proxy_packets=%d",
636 profile.name, appBytesPerExchange, wireBytesPerExchange, wireAmplification, fx.packets.Load())
637 t.Logf("PERF recovery profile=%s blackout_samples=%d blackout_p50=%s blackout_max=%s rebind_samples=%d rebind_p50=%s rebind_max=%s",
638 profile.name, blackoutLatency.Count, blackoutLatency.P50, blackoutLatency.Max, rebindLatency.Count, rebindLatency.P50, rebindLatency.Max)
639 t.Logf("PERF proxy profile=%s stats=%s", profile.name, stats)
640
641 perfBoundedCleanup(t, fx, client, server, baselineGoroutines)
642}