derp,types,util: use bufio Peek+Discard for allocation-free fast reads (#19067)
Replace byte-at-a-time ReadByte loops with Peek+Discard in the DERP
read path. Peek returns a slice into bufio's internal buffer without
allocating, and Discard advances the read pointer without copying.
Introduce util/bufiox with a BufferedReader interface and ReadFull
helper that uses Peek+copy+Discard as an allocation-free alternative
to io.ReadFull.
- derp.ReadFrameHeader: replace 5× ReadByte with Peek(5)+Discard(5),
reading the frame type and length directly from the peeked slice.
Remove now-unused readUint32 helper.
name old ns/op new ns/op speedup
ReadFrameHeader-8 24.2 12.4 ~2x
(0 allocs/op in both)
- key.NodePublic.ReadRawWithoutAllocating: replace 32× ReadByte with
bufiox.ReadFull. Addresses the "Dear future" comment about switching
away from byte-at-a-time reads once a non-escaping alternative exists.
name old ns/op new ns/op speedup
NodeReadRawWithoutAllocating-8 140 43.6 ~3.2x
(0 allocs/op in both)
- derpserver.handleFramePing: replace io.ReadFull with bufiox.ReadFull.
Updates tailscale/corp#38509
Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
This commit is contained in:
@@ -34,6 +34,65 @@ type (
|
||||
Client = derp.Client
|
||||
)
|
||||
|
||||
func TestReadFrameHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input [5]byte
|
||||
wantType derp.FrameType
|
||||
wantLen uint32
|
||||
}{
|
||||
{
|
||||
name: "SendPacket",
|
||||
input: [5]byte{byte(derp.FrameSendPacket), 0x00, 0x00, 0x04, 0x00},
|
||||
wantType: derp.FrameSendPacket,
|
||||
wantLen: 1024,
|
||||
},
|
||||
{
|
||||
name: "KeepAlive",
|
||||
input: [5]byte{byte(derp.FrameKeepAlive), 0x00, 0x00, 0x00, 0x00},
|
||||
wantType: derp.FrameKeepAlive,
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "MaxLen",
|
||||
input: [5]byte{byte(derp.FrameRecvPacket), 0xff, 0xff, 0xff, 0xff},
|
||||
wantType: derp.FrameRecvPacket,
|
||||
wantLen: 0xffffffff,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
br := bufio.NewReader(bytes.NewReader(tt.input[:]))
|
||||
gotType, gotLen, err := derp.ReadFrameHeader(br)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFrameHeader: %v", err)
|
||||
}
|
||||
if gotType != tt.wantType {
|
||||
t.Errorf("type = %v, want %v", gotType, tt.wantType)
|
||||
}
|
||||
if gotLen != tt.wantLen {
|
||||
t.Errorf("len = %v, want %v", gotLen, tt.wantLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Verify zero allocations.
|
||||
buf := make([]byte, 4096)
|
||||
rd := bytes.NewReader(buf)
|
||||
br := bufio.NewReader(rd)
|
||||
got := testing.AllocsPerRun(1000, func() {
|
||||
rd.Reset(buf)
|
||||
br.Reset(rd)
|
||||
_, _, err := derp.ReadFrameHeader(br)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFrameHeader: %v", err)
|
||||
}
|
||||
})
|
||||
if got != 0 {
|
||||
t.Fatalf("ReadFrameHeader allocs = %f, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientInfoUnmarshal(t *testing.T) {
|
||||
for i, in := range map[string]struct {
|
||||
json string
|
||||
|
||||
Reference in New Issue
Block a user