perf_evidence_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close