diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 1c253e5..74dd8e8 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -4,8 +4,8 @@ package pcap import ( "encoding/binary" "errors" - "fmt" "math" + "strconv" "strings" "unsafe" @@ -980,18 +980,22 @@ func (frm Frame) String() string { func (frm Frame) AppendString(b []byte) []byte { bitlen := frm.LenBits() - b = fmt.Appendf(b, "%s", frm.Protocol) + b = append(b, frm.Protocol...) if bitlen%8 == 0 { - b = fmt.Appendf(b, " len=%d", bitlen/8) + b = append(b, " len="...) + b = strconv.AppendInt(b, int64(bitlen/8), 10) } else { - b = fmt.Appendf(b, " bits=%d", bitlen) + b = append(b, " bits="...) + b = strconv.AppendInt(b, int64(bitlen), 10) } iopt, err := frm.FieldByClass(FieldClassOptions) if err == nil { - b = fmt.Appendf(b, " optlen=%d", (frm.Fields[iopt].BitLength+7)/8) + b = append(b, " optlen="...) + b = strconv.AppendInt(b, int64((frm.Fields[iopt].BitLength+7)/8), 10) } for _, err := range frm.Errors { - b = fmt.Appendf(b, " %s", err.Error()) + b = append(b, ' ') + b = append(b, err.Error()...) } return b } @@ -1005,10 +1009,10 @@ func (frm Frame) LenBits() (totalBitlen int) { func (ff FrameField) String() string { if ff.Class == FieldClassPayload { - return fmt.Sprintf("Payload len=%d", ff.BitLength/8) + return "Payload len=" + strconv.Itoa(ff.BitLength/8) } if ff.Name != "" { - return fmt.Sprintf("%s (%s)", ff.Name, ff.Class.String()) + return ff.Name + " (" + ff.Class.String() + ")" } return ff.Class.String() } diff --git a/internet/pcap/capture_bench_test.go b/internet/pcap/capture_bench_test.go new file mode 100644 index 0000000..b108860 --- /dev/null +++ b/internet/pcap/capture_bench_test.go @@ -0,0 +1,209 @@ +package pcap + +import ( + "testing" + "time" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/dhcp/dhcpv4" + "github.com/soypat/lneto/dns" + "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/ipv4" + "github.com/soypat/lneto/udp" +) + +const benchSubfieldLimit = 32 + +// buildDHCPPacket builds an Ethernet+IPv4+UDP+DHCPv4 Discover packet, exercising +// the option-heavy DHCP path (hostname, client id, requested address, param list). +func buildDHCPPacket(b testing.TB) []byte { + const ( + ethSize = 14 + ipv4Size = 20 + udpSize = 8 + ) + pkt := make([]byte, 600) + + efrm, _ := ethernet.NewFrame(pkt) + *efrm.DestinationHardwareAddr() = [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + *efrm.SourceHardwareAddr() = [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe} + efrm.SetEtherType(ethernet.TypeIPv4) + + ifrm, _ := ipv4.NewFrame(pkt[ethSize:]) + ifrm.SetVersionAndIHL(4, 5) + ifrm.SetID(0x1234) + ifrm.SetFlags(0x4000) + ifrm.SetTTL(64) + ifrm.SetProtocol(lneto.IPProtoUDP) + + ufrm, _ := udp.NewFrame(pkt[ethSize+ipv4Size:]) + ufrm.SetSourcePort(dhcpv4.DefaultClientPort) + ufrm.SetDestinationPort(dhcpv4.DefaultServerPort) + + var cl dhcpv4.Client + err := cl.BeginRequest(0xdeadbeef, dhcpv4.RequestConfig{ + RequestedAddr: [4]byte{192, 168, 1, 100}, + ClientHardwareAddr: [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe}, + Hostname: "myhost", + ClientID: "lneto-test", + }) + if err != nil { + b.Fatal("begin request:", err) + } + dhcpLen, err := cl.Encapsulate(pkt, ethSize, ethSize+ipv4Size+udpSize) + if err != nil { + b.Fatal("encapsulate:", err) + } + totalLen := ipv4Size + udpSize + dhcpLen + ifrm.SetTotalLength(uint16(totalLen)) + ufrm.SetLength(uint16(udpSize + dhcpLen)) + ifrm.SetCRC(ifrm.CalculateHeaderCRC()) + return pkt[:ethSize+totalLen] +} + +// buildDNSPacket builds an Ethernet+IPv4+UDP+DNS message with multiple questions +// and answers, exercising name encoding and resource record rendering. +func buildDNSPacket(b testing.TB) []byte { + const ( + ethSize = 14 + ipv4Size = 20 + udpSize = 8 + ) + var msg dns.Message + msg.Questions = []dns.Question{ + {Name: dns.MustNewName("example.com"), Type: dns.TypeA, Class: dns.ClassINET}, + {Name: dns.MustNewName("temu.com"), Type: dns.TypeAAAA, Class: dns.ClassANY}, + } + msg.Answers = []dns.Resource{ + dns.NewResource(dns.MustNewName("abc.com"), dns.TypeALL, dns.ClassANY, 64, []byte{10, 0, 11, 1}), + dns.NewResource(dns.MustNewName("123.com"), dns.TypeA, dns.ClassINET, 64, []byte{20, 0, 22, 2}), + } + dnsPayload, err := msg.AppendTo(nil, 0x1234, dns.NewClientHeaderFlags(dns.OpCodeQuery, true)) + if err != nil { + b.Fatal("dns encode:", err) + } + + pkt := make([]byte, ethSize+ipv4Size+udpSize+len(dnsPayload)) + + efrm, _ := ethernet.NewFrame(pkt) + *efrm.DestinationHardwareAddr() = [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x01} + *efrm.SourceHardwareAddr() = [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x02} + efrm.SetEtherType(ethernet.TypeIPv4) + + ifrm, _ := ipv4.NewFrame(pkt[ethSize:]) + ifrm.SetVersionAndIHL(4, 5) + ifrm.SetTTL(64) + ifrm.SetProtocol(lneto.IPProtoUDP) + ifrm.SetTotalLength(uint16(ipv4Size + udpSize + len(dnsPayload))) + ifrm.SetCRC(ifrm.CalculateHeaderCRC()) + + ufrm, _ := udp.NewFrame(pkt[ethSize+ipv4Size:]) + ufrm.SetSourcePort(58200) + ufrm.SetDestinationPort(dns.ServerPort) + ufrm.SetLength(uint16(udpSize + len(dnsPayload))) + + copy(pkt[ethSize+ipv4Size+udpSize:], dnsPayload) + return pkt +} + +func configureBenchFormatter(f *Formatter) { + f.SubfieldLimit = benchSubfieldLimit + f.FrameSep = "\n" + f.FieldSep = "; " + f.SubfieldSep = "\n\t" +} + +// BenchmarkPcap measures the decode, format, and decode+format (roundtrip) phases +// separately for the string-heavy DHCP and DNS frames. Run with -benchmem for +// per-phase allocs/op. +func BenchmarkPcap(b *testing.B) { + cases := []struct { + name string + pkt []byte + }{ + {"DHCP", buildDHCPPacket(b)}, + {"DNS", buildDNSPacket(b)}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + b.Run("decode", func(b *testing.B) { + var pb PacketBreakdown + pb.SubfieldLimit = benchSubfieldLimit + var frames []Frame + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + frames, _ = pb.CaptureEthernet(frames[:0], tc.pkt, 0) + } + }) + b.Run("format", func(b *testing.B) { + var pb PacketBreakdown + pb.SubfieldLimit = benchSubfieldLimit + frames, err := pb.CaptureEthernet(nil, tc.pkt, 0) + if err != nil { + b.Fatal(err) + } + var f Formatter + configureBenchFormatter(&f) + var buf []byte + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + buf, _ = f.FormatFrames(buf[:0], frames, tc.pkt) + } + }) + b.Run("roundtrip", func(b *testing.B) { + var pb PacketBreakdown + pb.SubfieldLimit = benchSubfieldLimit + var f Formatter + configureBenchFormatter(&f) + var frames []Frame + var buf []byte + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + frames, _ = pb.CaptureEthernet(frames[:0], tc.pkt, 0) + buf, _ = f.FormatFrames(buf[:0], frames, tc.pkt) + } + }) + }) + } +} + +// BenchmarkPcapPhases runs decode+format in a single benchmark loop while +// reporting per-phase wall time via custom metrics (decode-ns/op, format-ns/op). +// decode-ns/op + format-ns/op approximates ns/op minus time.Now overhead. +// Per-phase allocs are not split here (ReadMemStats is STW and skews timing); +// use BenchmarkPcap's decode/format sub-benchmarks with -benchmem for that. +func BenchmarkPcapPhases(b *testing.B) { + cases := []struct { + name string + pkt []byte + }{ + {"DHCP", buildDHCPPacket(b)}, + {"DNS", buildDNSPacket(b)}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + var pb PacketBreakdown + pb.SubfieldLimit = benchSubfieldLimit + var f Formatter + configureBenchFormatter(&f) + var frames []Frame + var buf []byte + var decNs, fmtNs int64 + b.ResetTimer() + for i := 0; i < b.N; i++ { + t0 := time.Now() + frames, _ = pb.CaptureEthernet(frames[:0], tc.pkt, 0) + t1 := time.Now() + buf, _ = f.FormatFrames(buf[:0], frames, tc.pkt) + t2 := time.Now() + decNs += t1.Sub(t0).Nanoseconds() + fmtNs += t2.Sub(t1).Nanoseconds() + } + b.ReportMetric(float64(decNs)/float64(b.N), "decode-ns/op") + b.ReportMetric(float64(fmtNs)/float64(b.N), "format-ns/op") + }) + } +} diff --git a/internet/stack-ethernet.go b/internet/stack-ethernet.go index 3a6360e..499ea3b 100644 --- a/internet/stack-ethernet.go +++ b/internet/stack-ethernet.go @@ -158,7 +158,12 @@ func (ls *StackEthernet) Demux(carrierData []byte, frameOffset int) (err error) return err } DROP: - ls.handlers.info("LinkStack:drop-packet", internal.SlogAddr6("dsthw", dstaddr), slog.String("ethertype", efrm.EtherTypeOrSize().String())) + // Frames not addressed to us are routine noise on a shared LAN (multicast/ + // broadcast spam). Log at debug so a production logger at info level gates it + // out instead of allocating slog attrs per dropped packet. + if internal.LogEnabled(ls.handlers.logger.log, slog.LevelDebug) { + ls.handlers.debug("LinkStack:drop-packet", internal.SlogAddr6("dsthw", dstaddr), slog.String("ethertype", efrm.EtherTypeOrSize().String())) + } return lneto.ErrPacketDrop } diff --git a/ipv4/icmpv4/client.go b/ipv4/icmpv4/client.go index 44af269..fd8d34c 100644 --- a/ipv4/icmpv4/client.go +++ b/ipv4/icmpv4/client.go @@ -38,6 +38,10 @@ type Client struct { raddr [4]byte } responseRing internal.Ring + // addrScratch holds the remote address for SetIPAddrs during Encapsulate. + // Kept on the (heap-resident) Client to avoid a per-call escape of a local + // [4]byte, which TinyGo would otherwise heap-allocate on every poll. + addrScratch [4]byte } type ClientConfig struct { @@ -155,7 +159,6 @@ func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) // Put n bytes of ICMP data. var n int - var raddr [4]byte if len(client.incomingEcho) > 0 { // Priority: send echo reply.1 inc := client.incomingEcho[0] @@ -170,7 +173,7 @@ func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) } client.incomingEcho = slices.Delete(client.incomingEcho, 0, 1) n = sizeHeader + dataLen - raddr = inc.raddr + client.addrScratch = inc.raddr } else if len(client.outgoingEcho) > 0 { idx := 0 for idx < len(client.outgoingEcho) { @@ -199,7 +202,7 @@ func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) copy(data[written:written+size%len(pattern)], pattern) n = sizeHeader + size out.key |= keyHashSentBit - raddr = out.raddr + client.addrScratch = out.raddr } else { return 0, nil } @@ -210,7 +213,7 @@ func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) sum := crc.PayloadSum16(carrierData[frameOffset : frameOffset+n]) ifrm.SetCRC(sum) if ipOffset >= 0 { - err = internal.SetIPAddrs(carrierData[ipOffset:], 0, nil, raddr[:]) + err = internal.SetIPAddrs(carrierData[ipOffset:], 0, nil, client.addrScratch[:]) } return n, err }