mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
perf(amneziawg): return gVisor's pooled buffers on the embedded data path
Every packet crossing the embedded AmneziaWG interface allocated instead of reusing gVisor's pools, in both directions. stackTun.Write injected each decrypted packet and never called DecRef, so the packet buffer and its chunk were never returned; stackTun.Read copied each view out and never released it. gVisor's own link endpoints settle the ownership question -- loopback.go and sharedmem.go both DecRef immediately after DeliverNetworkPacket, because the injector owns the buffer. AttachUDPHandler compounded it by cloning a packet buffer it then dropped on the floor, on top of a Data().AsRange().ToSlice() that already returns an owned copy, so the clone bought nothing and stranded a pooled buffer plus a cloned view per datagram. Measured with the benchmarks added here: stackTunWrite (upload) 794ns -> 107ns 4 -> 0 allocs stackTunRead (download) 707ns -> 129ns 3 -> 0 allocs UDP datagram, end to end 2.69us -> 1.58us 8 -> 2 allocs The remaining UDP allocation is the ToSlice copy itself. Through a real handshaked tunnel -- both devices in one process over loopback, so ChaCha20-Poly1305 and the UDP syscalls dominate -- it is worth -48% bytes/op and -33% allocs/op, and about +4.8% throughput in each direction (n=18, p<=0.01). On a small VPS, where the allocation pressure is not spread over 24 idle cores, the throughput share should be larger; that part is reasoning, not something measured here. The three regression tests assert allocations per packet rather than timing, since the defect is the pool miss, not the nanoseconds. Thresholds leave room for the extra allocation -race adds.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
|
||||
)
|
||||
|
||||
// TestStackTunReadDrainsBufferedBatch is a regression test for a real
|
||||
@@ -86,3 +87,44 @@ func TestStackTunReadStopsAtBufCapacity(t *testing.T) {
|
||||
t.Errorf("leftover packet = %v, want [3]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStackTunWriteReturnsPacketBuffersToPool locks in gVisor's ownership rule
|
||||
// for the upload path: whoever calls InjectInbound must DecRef the packet.
|
||||
func TestStackTunWriteReturnsPacketBuffersToPool(t *testing.T) {
|
||||
tun := &stackTun{ep: channel.New(tunQueueDepth, 1420, ""), mtu: 1420}
|
||||
defer tun.ep.Close()
|
||||
|
||||
packet := make([]byte, 1400)
|
||||
packet[0] = 0x45 // IPv4, version nibble is all Write inspects
|
||||
bufs := [][]byte{packet}
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
if _, err := tun.Write(bufs, 0); err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
})
|
||||
// 0 once pooled, 4 when every packet buffer is stranded; -race adds ~1.
|
||||
if allocs > 1 {
|
||||
t.Fatalf("Write allocates %v times per packet, want <=1: injected packet buffers are not being returned to gVisor's pools", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStackTunReadReturnsViewsToPool is the download-path counterpart: a view
|
||||
// that is copied out but never released strands its pooled chunk.
|
||||
func TestStackTunReadReturnsViewsToPool(t *testing.T) {
|
||||
tun := &stackTun{incomingPacket: make(chan *buffer.View, tunQueueDepth)}
|
||||
packet := make([]byte, 1400)
|
||||
buf := [][]byte{make([]byte, 2048)}
|
||||
sizes := make([]int, 1)
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
tun.incomingPacket <- buffer.NewViewWithData(packet)
|
||||
if _, err := tun.Read(buf, sizes, 0); err != nil {
|
||||
t.Fatalf("Read: %v", err)
|
||||
}
|
||||
})
|
||||
// 0 once the drained view goes back to viewPool, 3 when it does not.
|
||||
if allocs > 1 {
|
||||
t.Fatalf("Read allocates %v times per packet, want <=1: drained views are not being released", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user