Reduce heap allocs 2 (#46)

* reduce heap allocations in tcp logging; omit use of AppendFloat which allocates a metric sh*tton

* debugheaplog: better heap statistic logging

* heap: use string for pcap.Frame.Protocol

* add potential to eliminate Flags.String heap alloc, remove incorrect HEAP comments

* add StackAsync.DebugErr and httpraw.SetBytes

* many heap alloc reductions and replacement of bytes.Equal with internal.BytesEqual
This commit is contained in:
Pat Whittingslow
2026-02-28 19:20:46 +01:00
committed by GitHub
parent eab43c4653
commit 989bb6a0b9
22 changed files with 511 additions and 163 deletions
+80 -1
View File
@@ -1,7 +1,86 @@
package internal
import "log/slog"
import (
"log/slog"
"runtime"
"strconv"
"sync"
"unsafe"
)
const (
LevelTrace slog.Level = slog.LevelDebug - 2
usePrintLogAllocs = true
)
var (
memstats runtime.MemStats
lastAllocs uint64
lastMallocs uint64
allocmu sync.Mutex
allocbuf [256]byte
)
func LogAllocs(msg string) {
allocmu.Lock()
runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc == lastAllocs {
allocmu.Unlock()
return
}
if usePrintLogAllocs {
print("[ALLOC] ", msg)
print(" inc=", int64(memstats.TotalAlloc)-int64(lastAllocs))
print(" n=", int64(memstats.Mallocs)-int64(lastMallocs))
print(" heap=", memstats.HeapAlloc)
print(" tot=", memstats.TotalAlloc)
println()
} else {
n := copy(allocbuf[:], "[ALLOC] ")
n += copy(allocbuf[n:], msg)
n += copyValueInt(allocbuf[n:], "inc", int64(memstats.TotalAlloc)-int64(lastAllocs))
n += copyValueInt(allocbuf[n:], "n", int64(memstats.Mallocs)-int64(lastMallocs))
n += copyValueUint(allocbuf[n:], "heap", memstats.HeapAlloc)
n += copyValueUint(allocbuf[n:], "tot", memstats.TotalAlloc)
println(unsafe.String(&allocbuf[0], n))
}
lastAllocs = memstats.TotalAlloc
lastMallocs = memstats.Mallocs
allocmu.Unlock()
}
func copyValueInt(buf []byte, key string, v int64) int {
// ' ' + key + '=' + up to 20 chars for int64
if len(buf) < 2+len(key)+20 {
return 0
}
buf[0] = ' '
n := 1 + copy(buf[1:], key)
buf[n] = '='
n++
return n + copyInt(buf[n:], v)
}
func copyValueUint(buf []byte, key string, v uint64) int {
if len(buf) < 2+len(key)+20 {
return 0
}
buf[0] = ' '
n := 1 + copy(buf[1:], key)
buf[n] = '='
n++
return n + copyUint(buf[n:], v)
}
// copyInt formats v into buf and returns the number of bytes written.
// Caller must ensure cap(buf) >= 20.
func copyInt(buf []byte, v int64) int {
return len(strconv.AppendInt(buf[:0], v, 10))
}
// copyUint formats v into buf and returns the number of bytes written.
// Caller must ensure cap(buf) >= 20.
func copyUint(buf []byte, v uint64) int {
return len(strconv.AppendUint(buf[:0], v, 10))
}
+7 -11
View File
@@ -15,9 +15,6 @@ const (
)
var (
memstats runtime.MemStats
lastAllocs uint64
timebuf [len(timefmt) * 2]byte
)
@@ -28,12 +25,7 @@ func LogEnabled(l *slog.Logger, lvl slog.Level) bool {
func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) {
now := time.Now()
n := len(now.AppendFormat(timebuf[:0], timefmt))
runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc != lastAllocs {
print("[ALLOC] inc=", int64(memstats.TotalAlloc)-int64(lastAllocs))
print(" tot=", memstats.TotalAlloc, " seqs")
println()
}
LogAllocs(msg)
print("time=", unsafe.String(&timebuf[0], n), " ")
if level == LevelTrace {
print("TRACE ")
@@ -57,8 +49,12 @@ func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr)
}
}
println()
allocmu.Lock()
runtime.ReadMemStats(&memstats)
if memstats.TotalAlloc != lastAllocs {
lastAllocs = memstats.TotalAlloc
if lastAllocs != memstats.TotalAlloc {
print("alloc increase in heaplog")
}
lastAllocs = memstats.TotalAlloc
lastMallocs = memstats.Mallocs
allocmu.Unlock()
}
+3 -3
View File
@@ -1,12 +1,12 @@
package ltesto
import (
"bytes"
"math"
"math/rand"
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/tcp"
@@ -134,9 +134,9 @@ func (gen *PacketGen) AppendRandomIPv4TCPPacket(dst []byte, rng *rand.Rand, seg
switch {
case gen.SrcTCP != tfrm.SourcePort():
panic("IP options overwrite TCP header")
case !bytes.Equal(ifrm.Options(), ipOpts):
case !internal.BytesEqual(ifrm.Options(), ipOpts):
panic("bad ip options written, ensure ip options length is multiple of 4")
case !bytes.Equal(tfrm.Options(), tcpOpts):
case !internal.BytesEqual(tfrm.Options(), tcpOpts):
panic("bad tcp options written, ensure tcp options length is multiple of 4")
case *ifrm.DestinationAddr() != gen.DstIPv4:
panic("IP options overwrite own header")
+14
View File
@@ -1,5 +1,7 @@
package internal
import "unsafe"
// IsZeroed returns true if all arguments are set to their zero value.
func IsZeroed[T comparable](a ...T) bool {
var z T
@@ -65,3 +67,15 @@ func SliceReclaim[T any](ptr *[]T) *T {
}
return &(*ptr)[n]
}
// BytesEqual is heapless replacement of [bytes.Equal] since it allocates in tinygo.
// https://github.com/tinygo-org/tinygo/issues/4045
func BytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
if len(a) == 0 {
return true
}
return unsafe.String(&a[0], len(a)) == unsafe.String(&b[0], len(b))
}