From 376e1a0b4fa02116d16137489a7e89724de53610 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Sun, 15 Mar 2026 00:11:59 +0100 Subject: [PATCH] Further reduce heap allocs (#56) * work on tracking more heap allocations down * write own heapless IP AppendFormatAddr functions * remove potential Error method allocations * more logging --- internet/pcap/capture.go | 49 +++++++++++++++++--------- internet/pcap/format.go | 15 ++++++-- ipv4/definitions.go | 14 ++++++++ ipv4/definitions_test.go | 43 +++++++++++++++++++++++ ipv6/definitions.go | 55 +++++++++++++++++++++++++++++ ipv6/definitions_test.go | 74 ++++++++++++++++++++++++++++++++++++++++ tcp/frame.go | 8 ++--- validation.go | 10 ++++++ 8 files changed, 245 insertions(+), 23 deletions(-) create mode 100644 ipv4/definitions_test.go create mode 100644 ipv6/definitions_test.go diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 4440d03..0c2259f 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -172,31 +172,31 @@ func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) ( debuglog("pcap:ipv6:reclaimed") proto := ifrm6.NextHeader() end := bitOffset + 40*octet - var protoErrs []error + var protoErr error var crc lneto.CRC791 ifrm6.CRCWritePseudo(&crc) switch proto { case lneto.IPProtoTCP: if crc.PayloadSum16(ifrm6.Payload()) != 0 { - protoErrs = append(protoErrs, lneto.ErrBadCRC) + protoErr = lneto.ErrBadCRC } case lneto.IPProtoUDP, lneto.IPProtoUDPLite: ufrm, err := udp.NewFrame(ifrm6.Payload()) if err != nil { - protoErrs = append(protoErrs, err) + protoErr = err break } ufrm.ValidateSize(pc.validator()) if err = pc.validator().ErrPop(); err != nil { - protoErrs = append(protoErrs, err) + protoErr = err break } frameLen := ufrm.Length() if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 { - protoErrs = append(protoErrs, lneto.ErrBadCRC) + protoErr = lneto.ErrBadCRC } } - return pc.captureIPProto(proto, dst, pkt, end, protoErrs...) + return pc.captureIPProto(proto, dst, pkt, end, protoErr) } func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { @@ -231,8 +231,9 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( debuglog("pcap:ipv4:crc-done") proto := ifrm4.Protocol() end := bitOffset + octet*ifrm4.HeaderLength() - var protoErrs []error var crc lneto.CRC791 + + var ipProtoErr error payload := ifrm4.Payload() switch proto { case lneto.IPProtoTCP: @@ -244,7 +245,7 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( } ifrm4.CRCWriteTCPPseudo(&crc) if crc.PayloadSum16(payload) != 0 { - protoErrs = append(protoErrs, lneto.ErrBadCRC) + ipProtoErr = lneto.ErrBadCRC } } case lneto.IPProtoUDP: @@ -258,7 +259,7 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( frameLen := ufrm.Length() ifrm4.CRCWriteUDPPseudo(&crc, frameLen) if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 { - protoErrs = append(protoErrs, lneto.ErrBadCRC) + ipProtoErr = lneto.ErrBadCRC } } } @@ -266,15 +267,15 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( _, err := icmpv4.NewFrame(payload) if err == nil { if crc.PayloadSum16(payload) != 0 { - protoErrs = append(protoErrs, lneto.ErrBadCRC) + ipProtoErr = lneto.ErrBadCRC } } } debuglog("pcap:ipv4:proto-crc-done") - return pc.captureIPProto(proto, dst, pkt, end, protoErrs...) + return pc.captureIPProto(proto, dst, pkt, end, ipProtoErr) } -func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt []byte, bitOffset int, ipProtoErrs ...error) (_ []Frame, err error) { +func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt []byte, bitOffset int, ipProtoErr error) (_ []Frame, err error) { debuglog("pcap:ipproto:start") nextFrame := len(dst) switch proto { @@ -292,8 +293,8 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt default: reclaimRemainingFrame(&dst, "unknown proto", 0, bitOffset, octet*len(pkt)) } - if len(ipProtoErrs) > 0 && len(dst) > nextFrame { - dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...) + if ipProtoErr != nil && len(dst) > nextFrame { + dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErr) } debuglog("pcap:ipproto:done") return dst, err @@ -441,15 +442,19 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([ if err != nil && !incomplete { return dst, err } + debuglog("pcap:dns-decode") finfo := reclaimFrame(&dst, "DNS", bitOffset, nil) if incomplete { finfo.Errors = append(finfo.Errors, ErrLimitExceeded) } - finfo.Fields = append(finfo.Fields[:0], FrameField{ + field := internal.SliceReclaim(&finfo.Fields) + *field = FrameField{ Name: "Data", FrameBitOffset: 0, BitLength: int(off) * octet, - }) + SubFields: field.SubFields[:0], // Reuse subfields. + } + debuglog("pcap:dns-done") return dst, nil } @@ -463,6 +468,7 @@ func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([ return dst, err } reclaimFrame(&dst, "NTP", bitOffset, baseNTPFields[:]) + debuglog("pcap:ntp") return dst, nil } @@ -490,6 +496,7 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) SubFields: optfield.SubFields[:0], Name: "options", } + debuglog("pcap:dhcp-opt0") err = dfrm.ForEachOption(func(optoff int, opt dhcpv4.OptNum, data []byte) error { if len(optfield.SubFields) >= pc.SubfieldLimit { return ErrLimitExceeded @@ -541,11 +548,13 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) optfield.SubFields = append(optfield.SubFields, field) return nil }) + debuglog("pcap:dhcp-opt1") // optfield already in finfo.Fields via SliceReclaim, no append needed. if err != nil { finfo.Errors = append(finfo.Errors, err) } } + debuglog("pcap:dhcp-done") return dst, nil } @@ -739,6 +748,7 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) } // Optimized path: field starts at byte boundary. dst = append(dst, pkt[octetsStart:octetsStart+octets]...) + debuglog("pcap:appendField:optpath") if lastOctetExcessBits != 0 { dst[len(dst)-1] >>= lastOctetExcessBits } @@ -750,7 +760,9 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) if lastOctetExcessBits == 0 { // Right aligned with no loose trailing bits. i.e: TCP flags. dst = append(dst, pkt[octetsStart]&mask) + debuglog("pcap:appendField:rightalign1") dst = append(dst, pkt[octetsStart+1:octetsStart+octets]...) + debuglog("pcap:appendField:rightalign2") return dst, nil } // Right aligned with trailing bits. i.e: IPv6 Traffic Class. @@ -762,6 +774,7 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) b := (pkt[octetsStart+i] & mask) << (8 - firstBitOffset) b |= pkt[octetsStart+i+1] >> firstBitOffset dst = append(dst, b) + debuglog("pcap:appendField:rightalign3") } return dst, nil } @@ -772,10 +785,12 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) b := pkt[i+octetsStart] & mask b |= pkt[i+octetsStart+1] >> firstBitOffset dst = append(dst, b) + debuglog("pcap:appendField:leftalign1") } lastOctet := pkt[octetsStart+octets-1] & mask lastOctet >>= lastOctetExcessBits dst = append(dst, lastOctet) + debuglog("pcap:appendField:leftalign2") return dst, nil } @@ -1341,6 +1356,7 @@ func reclaimFrame(dst *[]Frame, proto string, bitOffset int, baseFields []FrameF Fields: append(finfo.Fields[:0], baseFields...), Errors: finfo.Errors[:0], } + debuglog("pcap:reclaim") return finfo } @@ -1352,6 +1368,7 @@ func reclaimRemainingFrame(dst *[]Frame, proto string, class FieldClass, pktBitO Class: class, BitLength: pktBitLen - pktBitOffset, }) + debuglog("pcap:reclaim-rem") } const enableDebug = internal.HeapAllocDebugging diff --git a/internet/pcap/format.go b/internet/pcap/format.go index 6b1ee7d..02bb1cb 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -4,7 +4,6 @@ import ( "encoding/binary" "encoding/hex" "math" - "net/netip" "slices" "strconv" "strings" @@ -14,6 +13,8 @@ import ( "github.com/soypat/lneto" "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/ipv4" + "github.com/soypat/lneto/ipv6" "github.com/soypat/lneto/ntp" "github.com/soypat/lneto/tcp" ) @@ -65,6 +66,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er dst = append(dst, " bitlen="...) dst = strconv.AppendInt(dst, int64(bitlen), 10) } + debuglog("pcap:FormatFrame:int") for ifield := range frm.Fields { field := frm.Fields[ifield] @@ -94,6 +96,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er dst = append(dst, ';') } dst = append(dst, err.Error()...) + debuglog("pcap:fmtframe:error") } dst = append(dst, ')') } @@ -122,6 +125,7 @@ func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, p dst, err = f.formatField(dst, pktStartOff, field.SubFields[i], pkt) } } + debuglog("pcap:FormatField:end") return dst, err } @@ -181,15 +185,20 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p return dst, err } dst = strconv.AppendUint(dst, v, 10) + debuglog("pcap:fmtfield:uint") } else if field.BitLength == 4*8 { - dst = netip.AddrFrom4([4]byte(f.buf)).AppendTo(dst) + dst = ipv4.AppendFormatAddr(dst, [4]byte(f.buf)) + debuglog("pcap:fmtfield:addr4") } else if field.BitLength == 6*8 { dst = ethernet.AppendAddr(dst, [6]byte(f.buf)) + debuglog("pcap:fmtfield:addr6") } else if field.BitLength == 16*8 { - dst = netip.AddrFrom16([16]byte(f.buf)).AppendTo(dst) + dst = ipv6.AppendFormatAddr(dst, [16]byte(f.buf)) + debuglog("pcap:fmtfield:addr16") } else { dst = append(dst, "0x"...) dst = hex.AppendEncode(dst, f.buf) + debuglog("pcap:fmtfield:addrN") } } return dst, err diff --git a/ipv4/definitions.go b/ipv4/definitions.go index d0bf309..266fef2 100644 --- a/ipv4/definitions.go +++ b/ipv4/definitions.go @@ -1,5 +1,7 @@ package ipv4 +import "strconv" + const ( sizeHeader = 20 ) @@ -68,3 +70,15 @@ func b2u8(b bool) uint8 { } return 0 } + +// AppendFormatAddr appends the dotted-decimal text representation of an IPv4 address +// to dst. Zero heap allocations. +func AppendFormatAddr(dst []byte, addr [4]byte) []byte { + for i, b := range addr { + if i != 0 { + dst = append(dst, '.') + } + dst = strconv.AppendUint(dst, uint64(b), 10) + } + return dst +} diff --git a/ipv4/definitions_test.go b/ipv4/definitions_test.go new file mode 100644 index 0000000..96b402b --- /dev/null +++ b/ipv4/definitions_test.go @@ -0,0 +1,43 @@ +package ipv4 + +import ( + "net/netip" + "testing" +) + +func TestAppendFormatAddr(t *testing.T) { + tests := []struct { + addr [4]byte + want string + }{ + {addr: [4]byte{0, 0, 0, 0}, want: "0.0.0.0"}, + {addr: [4]byte{127, 0, 0, 1}, want: "127.0.0.1"}, + {addr: [4]byte{192, 168, 1, 1}, want: "192.168.1.1"}, + {addr: [4]byte{255, 255, 255, 255}, want: "255.255.255.255"}, + {addr: [4]byte{10, 0, 0, 1}, want: "10.0.0.1"}, + {addr: [4]byte{1, 2, 3, 4}, want: "1.2.3.4"}, + {addr: [4]byte{100, 99, 9, 0}, want: "100.99.9.0"}, + } + for _, tc := range tests { + got := string(AppendFormatAddr(nil, tc.addr)) + if got != tc.want { + t.Errorf("AppendFormatAddr(%v): got %q, want %q", tc.addr, got, tc.want) + } + // Cross-check with netip. + want := netip.AddrFrom4(tc.addr).String() + if got != want { + t.Errorf("AppendFormatAddr(%v) disagrees with netip: got %q, want %q", tc.addr, got, want) + } + } +} + +func TestAppendFormatAddr_noAllocs(t *testing.T) { + var buf [24]byte + addr := [4]byte{192, 168, 1, 1} + allocs := testing.AllocsPerRun(100, func() { + _ = AppendFormatAddr(buf[:0], addr) + }) + if allocs != 0 { + t.Errorf("expected 0 allocs, got %v", allocs) + } +} diff --git a/ipv6/definitions.go b/ipv6/definitions.go index 84655f3..50c0ff7 100644 --- a/ipv6/definitions.go +++ b/ipv6/definitions.go @@ -7,3 +7,58 @@ const ( ) type ToS = ipv4.ToS + +// AppendFormatAddr appends the canonical text representation of an IPv6 address +// to dst following RFC 5952 conventions (lowercase hex, :: compression for the +// longest run of consecutive zero groups of length ≥ 2). Zero heap allocations. +func AppendFormatAddr(dst []byte, addr [16]byte) []byte { + const hexDigits = "0123456789abcdef" + + // Find the longest run of consecutive all-zero 16-bit groups for :: compression. + bestStart, bestLen := 0, 0 + curStart := -1 + for i := 0; i < 8; i++ { + if addr[i*2] == 0 && addr[i*2+1] == 0 { + if curStart < 0 { + curStart = i + } + if i-curStart+1 > bestLen { + bestStart = curStart + bestLen = i - curStart + 1 + } + } else { + curStart = -1 + } + } + if bestLen < 2 { + bestLen = 0 // RFC 5952 §4.2.2: do not compress a single 16-bit group. + } + + needColon := false + for i := 0; i < 8; i++ { + if bestLen > 0 && i == bestStart { + dst = append(dst, ':', ':') + i += bestLen - 1 // skip compressed groups; loop increments i. + needColon = false + continue + } + if needColon { + dst = append(dst, ':') + } + needColon = true + hi := addr[i*2] + lo := addr[i*2+1] + v := uint16(hi)<<8 | uint16(lo) + if v >= 0x1000 { + dst = append(dst, hexDigits[hi>>4]) + } + if v >= 0x100 { + dst = append(dst, hexDigits[hi&0xf]) + } + if v >= 0x10 { + dst = append(dst, hexDigits[lo>>4]) + } + dst = append(dst, hexDigits[lo&0xf]) + } + return dst +} diff --git a/ipv6/definitions_test.go b/ipv6/definitions_test.go new file mode 100644 index 0000000..228fbdd --- /dev/null +++ b/ipv6/definitions_test.go @@ -0,0 +1,74 @@ +package ipv6 + +import ( + "net/netip" + "testing" +) + +func TestAppendFormatAddr(t *testing.T) { + tests := []struct { + addr [16]byte + want string + }{ + // All zeros → "::". + {addr: [16]byte{}, want: "::"}, + // Loopback → "::1". + {addr: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, want: "::1"}, + // Full address, no compression. + {addr: [16]byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06}, want: "2001:db8:1:2:3:4:5:6"}, + // Trailing zero run. + {addr: [16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, want: "fe80::"}, + // Leading non-zero + middle compression. + {addr: [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, want: "2001:db8::1"}, + // Link-local with interface ID. + {addr: [16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, want: "fe80::1"}, + // Two zero runs; compress the longer one (groups 3-6 len=4 vs group 1 len=1). + {addr: [16]byte{0x20, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, want: "2001:0:1::1"}, + // Single zero group should NOT compress (RFC 5952 §4.2.2). + {addr: [16]byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06}, want: "1:0:1:2:3:4:5:6"}, + // All ff → no compression. + {addr: [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, want: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}, + // IPv4-mapped ::ffff:192.168.1.1 — we format as pure hex groups. + {addr: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xc0, 0xa8, 0x01, 0x01}, want: "::ffff:c0a8:101"}, + // Two equal-length zero runs; first one wins. + {addr: [16]byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, want: "1::1:2:0:0:1"}, + } + + for _, tc := range tests { + got := string(AppendFormatAddr(nil, tc.addr)) + if got != tc.want { + t.Errorf("AppendFormatAddr(%v):\n got %q\n want %q", tc.addr, got, tc.want) + } + } +} + +func TestAppendFormatAddr_matchesNetip(t *testing.T) { + // Verify output matches netip.Addr.AppendTo for non-IPv4-mapped addresses. + addrs := [][16]byte{ + {}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + {0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x01}, + {0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xfb}, + {0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06}, + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + } + for _, addr := range addrs { + got := string(AppendFormatAddr(nil, addr)) + want := netip.AddrFrom16(addr).String() + if got != want { + t.Errorf("mismatch for %v:\n got %q\n want %q", addr, got, want) + } + } +} + +func TestAppendFormatAddr_noAllocs(t *testing.T) { + var buf [64]byte + addr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + allocs := testing.AllocsPerRun(100, func() { + _ = AppendFormatAddr(buf[:0], addr) + }) + if allocs != 0 { + t.Errorf("expected 0 allocs, got %v", allocs) + } +} diff --git a/tcp/frame.go b/tcp/frame.go index e5ce8f8..9122962 100644 --- a/tcp/frame.go +++ b/tcp/frame.go @@ -187,19 +187,19 @@ func (tfrm Frame) String() string { func (tfrm Frame) ValidateSize(v *lneto.Validator) { off := tfrm.HeaderLength() if off < sizeHeaderTCP { - v.AddBitPosErr(12*8, 4, lneto.ErrInvalidLengthField) + v.AddError(lneto.ErrInvalidLengthField) } if off > len(tfrm.RawData()) { - v.AddBitPosErr(12*8, 4, lneto.ErrInvalidLengthField) + v.AddError(lneto.ErrShortBuffer) } } func (tfrm Frame) ValidateExceptCRC(v *lneto.Validator) { tfrm.ValidateSize(v) if tfrm.DestinationPort() == 0 { - v.AddBitPosErr(2*8, 16, lneto.ErrZeroDestination) + v.AddError(lneto.ErrZeroDestination) } if tfrm.SourcePort() == 0 { - v.AddBitPosErr(0, 16, lneto.ErrZeroSource) + v.AddError(lneto.ErrZeroSource) } } diff --git a/validation.go b/validation.go index 11fca69..24a6e6f 100644 --- a/validation.go +++ b/validation.go @@ -3,6 +3,7 @@ package lneto import ( "errors" "fmt" + "strconv" ) type ValidateFlags uint64 @@ -83,3 +84,12 @@ type BitPosErr struct { func (bpe *BitPosErr) Error() string { return fmt.Sprintf("%s at bits %d..%d", bpe.Err.Error(), bpe.BitStart, bpe.BitStart+bpe.BitLen) } + +func (bpe *BitPosErr) AppendError(dst []byte) []byte { + dst = append(dst, bpe.Err.Error()...) + dst = append(dst, ": bits "...) + dst = strconv.AppendUint(dst, uint64(bpe.BitStart), 10) + dst = append(dst, '.', '.') + dst = strconv.AppendUint(dst, uint64(bpe.BitStart+bpe.BitLen), 10) + return dst +}