mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
205 lines
5.2 KiB
Go
205 lines
5.2 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package transport
|
|
|
|
import (
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
embeddedProfileNodes = 32
|
|
embeddedProfileMaxHeapKB = 2048
|
|
embeddedProfileWarnHeapKB = 1024
|
|
embeddedMaxPathEntryBytes = 384
|
|
)
|
|
|
|
type simHeapDelta struct {
|
|
heapAlloc uint64
|
|
goroutines int
|
|
}
|
|
|
|
func measureSimNetworkHeap(t testing.TB, net *simNetwork, fn func()) simHeapDelta {
|
|
t.Helper()
|
|
runtime.GC()
|
|
time.Sleep(20 * time.Millisecond)
|
|
var m1 runtime.MemStats
|
|
runtime.ReadMemStats(&m1)
|
|
baseG := runtime.NumGoroutine()
|
|
|
|
fn()
|
|
|
|
runtime.GC()
|
|
time.Sleep(20 * time.Millisecond)
|
|
var m2 runtime.MemStats
|
|
runtime.ReadMemStats(&m2)
|
|
|
|
var used uint64
|
|
if m2.Alloc >= m1.Alloc {
|
|
used = m2.Alloc - m1.Alloc
|
|
}
|
|
return simHeapDelta{
|
|
heapAlloc: used,
|
|
goroutines: runtime.NumGoroutine() - baseG,
|
|
}
|
|
}
|
|
|
|
func TestSimEmbeddedPathTableBudget(t *testing.T) {
|
|
skipSimIfShort(t)
|
|
enableSimFastPath(t)
|
|
|
|
net := buildRandom(t, embeddedProfileNodes, 0.08, 0xebd00)
|
|
t.Cleanup(net.close)
|
|
|
|
delta := measureSimNetworkHeap(t, net, func() {
|
|
timeout := simConvergenceTimeout(net.diameter()) + 30*time.Second
|
|
for _, node := range net.nodes {
|
|
node.originateAnnounce(t)
|
|
time.Sleep(25 * time.Millisecond)
|
|
}
|
|
for i, src := range net.nodes {
|
|
peers := make([]*simNode, 0, len(net.nodes)-1)
|
|
for j, n := range net.nodes {
|
|
if i != j {
|
|
peers = append(peers, n)
|
|
}
|
|
}
|
|
assertAllHavePath(t, peers, src.destHash, timeout)
|
|
}
|
|
})
|
|
|
|
heapKB := delta.heapAlloc / 1024
|
|
t.Logf("embedded profile: nodes=%d heap=%d KB goroutines_delta=%d",
|
|
embeddedProfileNodes, heapKB, delta.goroutines)
|
|
|
|
// Announce-storm Alloc deltas are GC-noisy across cold vs warm process
|
|
// runs (often ~1.3 MB steady, sometimes ~3 MB on first sample). Hard
|
|
// byte budgets live in TestSimMemoryFootprintAcrossNodes.
|
|
if heapKB > embeddedProfileWarnHeapKB {
|
|
t.Logf("WARNING: heap %d KB exceeds warn budget %d KB", heapKB, embeddedProfileWarnHeapKB)
|
|
}
|
|
if heapKB > embeddedProfileMaxHeapKB && !raceBuild {
|
|
t.Logf("NOTE: heap %d KB above max budget %d KB (noisy Alloc delta, not a fail)",
|
|
heapKB, embeddedProfileMaxHeapKB)
|
|
}
|
|
|
|
totalEntries := 0
|
|
for _, node := range net.nodes {
|
|
entries := simPathTableLen(node.tr)
|
|
totalEntries += entries
|
|
if entries > embeddedProfileNodes {
|
|
t.Errorf("node %s path count %d exceeds %d", node.name, entries, embeddedProfileNodes)
|
|
}
|
|
}
|
|
t.Logf("path entries across network=%d", totalEntries)
|
|
|
|
if per := pathEntrySize(net.nodes[0].tr, net.nodes[1].destHash); per > 0 {
|
|
t.Logf("spot-check path entry size: ~%d bytes", per)
|
|
if per > embeddedMaxPathEntryBytes && !raceBuild {
|
|
t.Errorf("per-entry size %d exceeds %d B budget", per, embeddedMaxPathEntryBytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSimEmbeddedGoroutineBudget(t *testing.T) {
|
|
skipSimIfShort(t)
|
|
enableSimFastPath(t)
|
|
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := runtime.NumGoroutine()
|
|
|
|
for range 3 {
|
|
net := buildRandom(t, embeddedProfileNodes, 0.08, 0x600d)
|
|
for _, node := range net.nodes {
|
|
node.originateAnnounce(t)
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
net.close()
|
|
}
|
|
|
|
runtime.GC()
|
|
time.Sleep(750 * time.Millisecond)
|
|
final := runtime.NumGoroutine()
|
|
if final > baseline+2 {
|
|
buf := make([]byte, 1<<20)
|
|
nb := runtime.Stack(buf, true)
|
|
t.Errorf("goroutine leak suspected: baseline=%d final=%d\n%s", baseline, final, buf[:nb])
|
|
}
|
|
t.Logf("goroutines: baseline=%d final=%d", baseline, final)
|
|
}
|
|
|
|
func TestSimPathTableGrowthBounded(t *testing.T) {
|
|
skipSimIfShort(t)
|
|
enableSimFastPath(t)
|
|
|
|
const n = 32
|
|
net := buildLine(t, n)
|
|
t.Cleanup(net.close)
|
|
|
|
net.nodes[0].originateAnnounce(t)
|
|
timeout := simConvergenceTimeout(n - 1)
|
|
assertAllHavePath(t, net.nodes[1:], net.nodes[0].destHash, timeout)
|
|
|
|
for i, node := range net.nodes {
|
|
entries := simPathTableLen(node.tr)
|
|
if entries > n {
|
|
t.Errorf("node%d path table has %d entries, want <= %d", i, entries, n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSimMemoryFootprintAcrossNodes(t *testing.T) {
|
|
skipSimIfShort(t)
|
|
|
|
const n = 32
|
|
net := buildLine(t, n)
|
|
t.Cleanup(net.close)
|
|
|
|
var m1, m2 runtime.MemStats
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m1)
|
|
|
|
for _, src := range net.nodes {
|
|
ifaceName := src.ifaces[0].GetName()
|
|
for _, dst := range net.nodes {
|
|
if dst == src {
|
|
continue
|
|
}
|
|
src.tr.UpdatePath(dst.destHash, dst.destHash, ifaceName, 1)
|
|
}
|
|
}
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&m2)
|
|
|
|
var used uint64
|
|
if m2.Alloc >= m1.Alloc {
|
|
used = m2.Alloc - m1.Alloc
|
|
}
|
|
|
|
entries := uint64(n) * uint64(n-1)
|
|
perEntry := used / entries
|
|
heapKB := used / 1024
|
|
|
|
t.Logf("nodes=%d entries=%d total=%d KB per_entry=%d B",
|
|
n, entries, heapKB, perEntry)
|
|
|
|
// The race detector's instrumented allocator adds per-object bookkeeping
|
|
// that inflates runtime.MemStats.Alloc independently of actual
|
|
// application memory efficiency, so these byte-level budgets are only
|
|
// meaningful on a non-instrumented build.
|
|
if !raceBuild {
|
|
if heapKB > embeddedProfileMaxHeapKB {
|
|
t.Fatalf("path table footprint %d KB exceeds %d KB budget", heapKB, embeddedProfileMaxHeapKB)
|
|
}
|
|
if perEntry > embeddedMaxPathEntryBytes {
|
|
t.Fatalf("per-entry size %d B exceeds %d B budget", perEntry, embeddedMaxPathEntryBytes)
|
|
}
|
|
}
|
|
if heapKB > embeddedProfileWarnHeapKB {
|
|
t.Logf("WARNING: footprint %d KB exceeds warn budget %d KB", heapKB, embeddedProfileWarnHeapKB)
|
|
}
|
|
}
|