Reduce heap allocations (#42)

* pcap: reuse Frame memory

* slog: reduce heap allocations of addresses; also prevent heap alloc of dhcp options in pcap

* dns: heapless improvement; add StackAsync buffer for more heapless operation; start thinking of errors

* reuse function for reclaiming frames among all protocols for lower memory usage

* remove extraneous code
This commit is contained in:
Pat Whittingslow
2026-02-27 16:38:33 +01:00
committed by GitHub
parent 92b54487df
commit f9748da94f
11 changed files with 198 additions and 130 deletions
+90 -106
View File
@@ -14,6 +14,7 @@ import (
"github.com/soypat/lneto/dns"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/ipv6"
@@ -43,7 +44,31 @@ type PacketBreakdown struct {
SubfieldLimit int
}
// initFrames pre-allocates a []Frame with per-position Fields capacity
// sized for the protocol layers at each position. This avoids heap
// allocations on the first packet when using SliceReclaim on subsequent calls.
//
// Pre-allocated capacities per position:
//
// 0: Ethernet (3 base + VLAN tag = 4)
// 1: L3 max(IPv4=12, ARP=9, IPv6=8) = 12
// 2: L4 max(TCP=10, ICMP=8, UDP=4) = 10
// 3: App max(DHCP=15, NTP=13, HTTP=2, DNS=1) = 16
// 4-5: overflow/remaining = 2
func (pc *PacketBreakdown) initFrames() []Frame {
const nframes = 6
var fieldCaps = [nframes]int{4, 12, 10, 16, 2, 2}
frames := make([]Frame, nframes)
for i := range frames {
frames[i].Fields = make([]FrameField, 0, fieldCaps[i])
}
return frames[:0]
}
func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if dst == nil {
dst = pc.initFrames()
}
if bitOffset%8 != 0 {
return dst, errors.New("ethernet must be parsed at byte boundary")
}
@@ -56,23 +81,17 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
return dst, pc.validator().ErrPop()
}
finfo := Frame{
Protocol: ProtoEthernet,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseEthernetFields[:]...)
finfo := reclaimFrame(&dst, ProtoEthernet, bitOffset, baseEthernetFields[:])
etype := efrm.EtherTypeOrSize()
end := 14*octet + bitOffset
if etype.IsSize() {
finfo.Fields[len(finfo.Fields)-1].Class = FieldClassSize
dst = append(dst, finfo)
dst = append(dst, remainingFrameInfo("Ethernet payload", FieldClassPayload, end, octet*len(pkt)))
reclaimRemainingFrame(&dst, "Ethernet payload", FieldClassPayload, end, octet*len(pkt))
return dst, nil
}
dst = append(dst, finfo)
if efrm.IsVLAN() {
finfo.Fields = append(finfo.Fields, FrameField{Name: "VLAN Tag", Class: FieldClassType, FrameBitOffset: end, BitLength: 2 * octet})
dst = append(dst, remainingFrameInfo("Ethernet VLAN", FieldClassPayload, end+2*octet, octet*len(pkt)))
reclaimRemainingFrame(&dst, "Ethernet VLAN", FieldClassPayload, end+2*octet, octet*len(pkt))
return dst, nil
}
switch etype {
@@ -83,7 +102,7 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in
case ethernet.TypeIPv6:
dst, err = pc.CaptureIPv6(dst, pkt, end)
default:
dst = append(dst, remainingFrameInfo(etype, FieldClassPayload, end, octet*len(pkt)))
reclaimRemainingFrame(&dst, etype, FieldClassPayload, end, octet*len(pkt))
}
return dst, err
}
@@ -101,13 +120,8 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([
return dst, pc.validator().ErrPop()
}
finfo := Frame{
Protocol: ethernet.TypeARP,
PacketBitOffset: bitOffset,
}
finfo := reclaimFrame(&dst, ethernet.TypeARP, bitOffset, baseARPFields[:])
const varstart = 8 * octet
finfo.Fields = append(finfo.Fields, baseARPFields[:]...)
_, hlen := afrm.Hardware()
_, plen := afrm.Protocol()
finfo.Fields = append(finfo.Fields,
@@ -136,7 +150,6 @@ func (pc *PacketBreakdown) CaptureARP(dst []Frame, pkt []byte, bitOffset int) ([
BitLength: int(plen) * octet,
},
)
dst = append(dst, finfo)
return dst, nil
}
@@ -152,12 +165,7 @@ func (pc *PacketBreakdown) CaptureIPv6(dst []Frame, pkt []byte, bitOffset int) (
if pc.validator().HasError() {
return dst, pc.validator().ErrPop()
}
finfo := Frame{
Protocol: ethernet.TypeIPv6,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseIPv6Fields[:]...)
dst = append(dst, finfo)
reclaimFrame(&dst, ethernet.TypeIPv6, bitOffset, baseIPv6Fields[:])
proto := ifrm6.NextHeader()
end := bitOffset + 40*octet
var protoErrs []error
@@ -201,11 +209,7 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
}
// limit packet to the actual IPv4 frame size
pkt = pkt[:bitOffset/8+int(ifrm4.TotalLength())]
finfo := Frame{
Protocol: ethernet.TypeIPv4,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseIPv4Fields[:]...)
finfo := reclaimFrame(&dst, ethernet.TypeIPv4, bitOffset, baseIPv4Fields[:])
options := ifrm4.Options()
if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{
@@ -217,7 +221,6 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) (
if ifrm4.CalculateHeaderCRC() != 0 {
finfo.Errors = append(finfo.Errors, lneto.ErrBadCRC)
}
dst = append(dst, finfo)
proto := ifrm4.Protocol()
end := bitOffset + octet*ifrm4.HeaderLength()
var protoErrs []error
@@ -277,7 +280,7 @@ func (pc *PacketBreakdown) captureIPProto(proto lneto.IPProto, dst []Frame, pkt
case lneto.IPProtoICMP:
dst, err = pc.CaptureICMPv4(dst, pkt, bitOffset)
default:
dst = append(dst, remainingFrameInfo(proto, 0, bitOffset, octet*len(pkt)))
reclaimRemainingFrame(&dst, proto, 0, bitOffset, octet*len(pkt))
}
if len(ipProtoErrs) > 0 && len(dst) > nextFrame {
dst[nextFrame].Errors = append(dst[nextFrame].Errors, ipProtoErrs...)
@@ -298,11 +301,7 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
return dst, pc.validator().ErrPop()
}
end := bitOffset + octet*tfrm.HeaderLength()
finfo := Frame{
Protocol: lneto.IPProtoTCP,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseTCPFields[:]...)
finfo := reclaimFrame(&dst, lneto.IPProtoTCP, bitOffset, baseTCPFields[:])
options := tfrm.Options()
if len(options) > 0 {
finfo.Fields = append(finfo.Fields, FrameField{
@@ -311,20 +310,16 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
BitLength: octet * len(options),
})
}
dst = append(dst, finfo)
payload := tfrm.Payload()
if len(payload) > 0 {
dst, err = pc.CaptureHTTP(dst, pkt, end)
if err != nil {
dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)))
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
}
}
return dst, nil
}
// func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
// }
func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return dst, errors.New("UDP must be parsed at byte boundary")
@@ -337,12 +332,7 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
if pc.validator().HasError() {
return dst, pc.validator().ErrPop()
}
finfo := Frame{
Protocol: lneto.IPProtoUDP,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseUDPFields[:]...)
dst = append(dst, finfo)
reclaimFrame(&dst, lneto.IPProtoUDP, bitOffset, baseUDPFields[:])
end := bitOffset + 8*octet
payload := ufrm.Payload()
dstport := ufrm.DestinationPort()
@@ -355,7 +345,7 @@ func (pc *PacketBreakdown) CaptureUDP(dst []Frame, pkt []byte, bitOffset int) ([
dst, err = pc.CaptureNTP(dst, pkt, end)
}
if err != nil {
dst = append(dst, remainingFrameInfo(unknownPayloadProto, FieldClassPayload, end, octet*len(pkt)))
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
}
return dst, nil
}
@@ -370,11 +360,7 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
return dst, err
}
finfo := Frame{
Protocol: lneto.IPProtoICMP,
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseICMPv4Fields[:]...)
finfo := reclaimFrame(&dst, lneto.IPProtoICMP, bitOffset, baseICMPv4Fields[:])
// Add type-specific fields.
switch ifrm.Type() {
@@ -421,13 +407,12 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
})
}
}
dst = append(dst, finfo)
return dst, nil
}
func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return nil, errors.New("DNS must be parsed at byte boundary")
return dst, errors.New("DNS must be parsed at byte boundary")
}
dnsData := pkt[bitOffset/8:]
pc.dmsg.LimitResourceDecoding(20, 20, 20, 20)
@@ -435,10 +420,7 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
if err != nil && !incomplete {
return dst, err
}
finfo := Frame{
Protocol: "DNS",
PacketBitOffset: bitOffset,
}
finfo := reclaimFrame(&dst, "DNS", bitOffset, nil)
if incomplete {
finfo.Errors = append(finfo.Errors, errors.New("pcap: could not parse all DNS resources; add higher limit"))
}
@@ -447,52 +429,46 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
FrameBitOffset: 0,
BitLength: int(off) * octet,
})
dst = append(dst, finfo)
return dst, nil
}
func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return nil, errors.New("NTP must be parsed at byte boundary")
return dst, errors.New("NTP must be parsed at byte boundary")
}
ntpData := pkt[bitOffset/8:]
_, err := ntp.NewFrame(ntpData)
if err != nil {
return dst, err
}
finfo := Frame{
Protocol: "NTP",
PacketBitOffset: bitOffset,
}
finfo.Fields = append(finfo.Fields, baseNTPFields[:]...)
dst = append(dst, finfo)
reclaimFrame(&dst, "NTP", bitOffset, baseNTPFields[:])
return dst, nil
}
func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
if bitOffset%8 != 0 {
return nil, errors.New("DHCP must be parsed at byte boundary")
return dst, errors.New("DHCP must be parsed at byte boundary")
}
dhcpData := pkt[bitOffset/8:]
dfrm, err := dhcpv4.NewFrame(dhcpData)
if err != nil {
return nil, err
}
finfo := Frame{
Protocol: "DHCPv4",
PacketBitOffset: bitOffset,
return dst, err
}
finfo := reclaimFrame(&dst, "DHCPv4", bitOffset, baseDHCPv4Fields[:])
magic := dfrm.MagicCookie()
if magic != dhcpv4.MagicCookie {
finfo.Errors = append(finfo.Errors, errors.New("incorrect DHCPv4 magic cookie"))
}
finfo.Fields = append(finfo.Fields, baseDHCPv4Fields[:]...)
options := dfrm.OptionsPayload()
if len(options) > 0 && pc.SubfieldLimit > 0 {
var optfield FrameField
optfield.Class = FieldClassOptions
optfield.Name = "options"
// Reclaim FrameField from Fields backing array to reuse its SubFields backing array.
optfield := internal.SliceReclaim(&finfo.Fields)
*optfield = FrameField{ // Ensure consistent zeroing of memory, but keep slice reference for reuse.
Class: FieldClassOptions,
SubFields: optfield.SubFields[:0],
Name: "options",
}
err = dfrm.ForEachOption(func(optoff int, opt dhcpv4.OptNum, data []byte) error {
if len(optfield.SubFields) >= pc.SubfieldLimit {
return errors.New("option cap limit surpassed for DHCP")
@@ -544,12 +520,11 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
optfield.SubFields = append(optfield.SubFields, field)
return nil
})
finfo.Fields = append(finfo.Fields, optfield)
// optfield already in finfo.Fields via SliceReclaim, no append needed.
if err != nil {
finfo.Errors = append(finfo.Errors, err)
}
}
dst = append(dst, finfo)
return dst, nil
}
@@ -589,7 +564,7 @@ func httpBodyClass(contentType, body []byte) FieldClass {
func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
const httpProtocol = "HTTP"
if bitOffset%8 != 0 {
return nil, errors.New("HTTP must be parsed at byte boundary")
return dst, errors.New("HTTP must be parsed at byte boundary")
}
const asResponse = true
const asRequest = false
@@ -606,25 +581,22 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
hdrLen := pc.hdr.BufferParsed()
body, _ := pc.hdr.Body()
bodyClass := httpBodyClass(pc.hdr.Get("Content-Type"), body)
dst = append(dst, Frame{
Protocol: httpProtocol,
PacketBitOffset: bitOffset,
Fields: []FrameField{
{
Name: "HTTP Header",
Class: FieldClassText,
FrameBitOffset: 0,
BitLength: hdrLen * octet,
},
{
Name: "HTTP Body",
Class: bodyClass,
FrameBitOffset: hdrLen * octet,
BitLength: len(body) * octet,
},
finfo := reclaimFrame(&dst, httpProtocol, bitOffset, nil)
finfo.Fields = append(finfo.Fields,
FrameField{
Name: "HTTP Header",
Class: FieldClassText,
FrameBitOffset: 0,
BitLength: hdrLen * octet,
},
})
return dst, err
FrameField{
Name: "HTTP Body",
Class: bodyClass,
FrameBitOffset: hdrLen * octet,
BitLength: len(body) * octet,
},
)
return dst, nil
}
func (pc *PacketBreakdown) validator() *lneto.Validator {
@@ -653,9 +625,9 @@ func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
type Frame struct {
PacketBitOffset int
Protocol any
Fields []FrameField
PacketBitOffset int
Errors []error
}
@@ -1331,14 +1303,26 @@ var baseNTPFields = [...]FrameField{
},
}
func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) Frame {
return Frame{
// reclaimFrame extends dst via [internal.SliceReclaim], resets the reclaimed Frame
// with the given protocol and bit offset while preserving Fields and Errors
// backing arrays, and appends baseFields. Returns the Frame for further modification.
func reclaimFrame(dst *[]Frame, proto any, bitOffset int, baseFields []FrameField) *Frame {
finfo := internal.SliceReclaim(dst)
*finfo = Frame{
PacketBitOffset: bitOffset,
Protocol: proto,
PacketBitOffset: pktBitOffset,
Fields: []FrameField{
{
Class: class,
BitLength: pktBitLen - pktBitOffset,
}},
Fields: append(finfo.Fields[:0], baseFields...),
Errors: finfo.Errors[:0],
}
return finfo
}
// reclaimRemainingFrame extends dst via SliceReclaim and populates the reclaimed
// Frame as a single-field "remaining payload" frame, reusing the old Fields backing array.
func reclaimRemainingFrame(dst *[]Frame, proto any, class FieldClass, pktBitOffset, pktBitLen int) {
finfo := reclaimFrame(dst, proto, pktBitOffset, nil)
finfo.Fields = append(finfo.Fields, FrameField{
Class: class,
BitLength: pktBitLen - pktBitOffset,
})
}
+43 -5
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math"
"net/netip"
"slices"
"strconv"
@@ -31,6 +32,7 @@ type Formatter struct {
DisableLegacyFilter bool
mubuf sync.Mutex
buf []byte
uintBuf [8]byte // scratch buffer for fieldAsUint to avoid TinyGo heap escape.
}
// FormatFrames appends the formatted frame data to the destination buffer according the Formatter state.
@@ -49,14 +51,17 @@ func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte
return dst, nil
}
// FormatFrame
// FormatFrame formats a single frame's protocol, fields, and errors into dst.
func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) {
sep := f.fieldSep()
bitlen := frm.LenBits()
dst = appendProtocol(dst, frm.Protocol)
if bitlen%8 == 0 {
dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8)
dst = append(dst, " len="...)
dst = strconv.AppendInt(dst, int64(bitlen/8), 10)
} else {
dst = fmt.Appendf(dst, "%s bitlen=%d", frm.Protocol, bitlen)
dst = append(dst, " bitlen="...)
dst = strconv.AppendInt(dst, int64(bitlen), 10)
}
for ifield := range frm.Fields {
@@ -68,7 +73,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP {
// TCP flags pretty print special case.
dst = append(dst, "flags="...)
v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
v, err := f.fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
if err != nil {
return dst, err
}
@@ -158,7 +163,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
// IP, MAC addresses and ports.
if field.BitLength <= 16 {
v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
v, err := f.fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
if err != nil {
return dst, err
}
@@ -198,3 +203,36 @@ func (f *Formatter) subfieldSep() string {
}
return sep
}
// fieldAsUint evaluates a packet field as a uint64 using the Formatter's
// scratch buffer to avoid a TinyGo heap escape from a local [8]byte.
func (f *Formatter) fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAligned bool) (uint64, error) {
const badUint64 = math.MaxUint64
octets := (bitlen + 7) / 8
if octets > 8 {
return badUint64, errors.New("field too long to be represented by uint64")
}
f.uintBuf = [8]byte{}
_, err := appendField(f.uintBuf[8-octets:8-octets], pkt, fieldBitStart, bitlen, rightAligned)
if err != nil {
return badUint64, err
}
return binary.BigEndian.Uint64(f.uintBuf[:]), nil
}
// appendProtocol appends the string representation of a Frame.Protocol value
// to dst without going through fmt.Appendf reflect machinery.
func appendProtocol(dst []byte, p any) []byte {
switch p := p.(type) {
case proto:
return append(dst, string(p)...)
case string:
return append(dst, p...)
case ethernet.Type:
return append(dst, p.String()...)
case lneto.IPProto:
return append(dst, p.String()...)
default:
return fmt.Appendf(dst, "%v", p)
}
}
+2 -2
View File
@@ -6,10 +6,10 @@ import (
"io"
"log/slog"
"math"
"net"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
)
// StackEthernetConfig contains configuration parameters for [StackEthernet].
@@ -132,7 +132,7 @@ func (ls *StackEthernet) Demux(carrierData []byte, frameOffset int) (err error)
return err
}
DROP:
ls.handlers.info("LinkStack:drop-packet", slog.String("dsthw", net.HardwareAddr(dstaddr[:]).String()), slog.String("ethertype", efrm.EtherTypeOrSize().String()))
ls.handlers.info("LinkStack:drop-packet", internal.SlogAddr6("dsthw", dstaddr), slog.String("ethertype", efrm.EtherTypeOrSize().String()))
return lneto.ErrPacketDrop
}
+1 -1
View File
@@ -104,7 +104,7 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error {
// nodeIdx := getNodeByProto(sb.handlers, uint16(proto))
if node == nil {
// Drop packet.
sb.handlers.info("ip:demux.drop", slog.String("dstaddr", netip.AddrFrom4(*ifrm.DestinationAddr()).String()), slog.String("proto", ifrm.Protocol().String()))
sb.handlers.info("ip:demux.drop", internal.SlogAddr4("dstaddr", ifrm.DestinationAddr()), slog.String("proto", ifrm.Protocol().String()))
return lneto.ErrPacketDrop
}
// Incoming CRC Validation of common IP Protocols.