Files
kms/conn_memory_test.go
zeekayandHanzo Dev 01d52f0aec
Hanzo CI/CD / cicd (push) Failing after 54m52s
CI/CD / cicd (push) Failing after 54m52s
Declare operations on the group that serves them
zip's registration is a verb method on the group, so an operation reads as the
group answering it rather than a function taking the group, and a route that is
not a typed operation says which method it answers with. The package-level form
and the Router interface it existed to accept any target through are both gone.

The toolchain and the pin move together, deliberately. Below zip v1.36.50 the
JSON encoder was selected on a build tag, so a repo whose go directive rises
first writes a wire that is neither the old release's nor the new one's until the
pin catches up.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 06:03:33 -07:00

182 lines
5.0 KiB
Go

// Copyright © 2026 Hanzo AI. MIT License.
// Per-connection memory profile for the KMS subsystem — canonical
// regression test per SCALE_STANDARD.md §3 / §8.
//
// We measure zip-on-fasthttp's per-conn overhead with KMS's health
// route registered, NOT the full Embed() server (which boots ZapDB,
// the ZAP node, and the replicator — those are out of scope for the
// per-conn budget). The fasthttp accept-loop and zip Ctx pool are
// the same regardless of which subsystem mounts on top, so this test
// catches the same regressions across services.
//
// Run with:
// go test -mod=mod -run=TestConnMemory -v -conn-count=10000
package kms_test
import (
"context"
"flag"
"fmt"
"net"
"net/http"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
var connCount = flag.Int("conn-count", 1000, "concurrent connections to hold")
const (
maxPerConnHeapBytes = 12 * 1024
goroutinesPerConnLow = 0.95
goroutinesPerConnHigh = 1.05
)
func TestConnMemory(t *testing.T) {
if testing.Short() {
t.Skip("skipping memory profile in -short mode")
}
n := *connCount
app := zip.New(zip.Config{
Logger: luxlog.New("test", "kms-conn-memory"),
DisableStartupMessage: true,
AppName: "kms",
})
// Mirror the canonical /v1/kms/health route shape (see mount.go).
app.Raw(http.MethodGet, "/v1/kms/health", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]any{
"status": "ok",
"service": "kms",
})
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var holding atomic.Int64
app.Raw(http.MethodGet, "/hold", func(c *zip.Ctx) error {
holding.Add(1)
defer holding.Add(-1)
<-ctx.Done()
return nil
})
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
addr := ln.Addr().String()
go func() { _ = app.Fiber().Listener(ln) }()
defer func() { _ = app.Shutdown() }()
time.Sleep(200 * time.Millisecond)
var baseline, peak runtime.MemStats
// Goroutines get a baseline for the same reason heap does. NumGoroutine is
// PROCESS-wide, so when this runs inside the full package it also counts
// badger compactors, httptest servers and the embedded instance other tests
// left running — the ratio read 1.00 alone and 1.15 in the suite, and the
// test failed for goroutines it does not own. The delta is what "per
// connection" can mean.
baselineGoroutines := runtime.NumGoroutine()
runtime.GC()
runtime.ReadMemStats(&baseline)
req := []byte("GET /hold HTTP/1.1\r\nHost: x\r\n\r\n")
conns := make([]net.Conn, 0, n)
var connsMu sync.Mutex
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
c, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
return
}
if _, err := c.Write(req); err != nil {
_ = c.Close()
return
}
connsMu.Lock()
conns = append(conns, c)
connsMu.Unlock()
}()
}
wg.Wait()
defer func() {
connsMu.Lock()
for _, c := range conns {
_ = c.Close()
}
connsMu.Unlock()
}()
deadline := time.Now().Add(30 * time.Second)
for holding.Load() < int64(n) && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
if got := holding.Load(); got < int64(n) {
t.Logf("only %d/%d conns accepted before deadline", got, n)
n = int(got)
}
if n == 0 {
t.Fatal("no conns accepted")
}
runtime.GC()
runtime.ReadMemStats(&peak)
delta := int64(peak.HeapAlloc) - int64(baseline.HeapAlloc)
perConn := float64(delta) / float64(n)
totalGoroutines := runtime.NumGoroutine()
goroutinesPerConn := float64(totalGoroutines-baselineGoroutines) / float64(n)
fmt.Printf("\n=== Per-connection memory profile (hanzoai/kms) ===\n")
fmt.Printf("conns held : %d\n", n)
fmt.Printf("baseline heap : %s\n", humanBytes(int64(baseline.HeapAlloc)))
fmt.Printf("peak heap : %s\n", humanBytes(int64(peak.HeapAlloc)))
fmt.Printf("delta : %s\n", humanBytes(delta))
fmt.Printf("per-conn heap : %.0f B (%.2f KiB)\n", perConn, perConn/1024)
fmt.Printf("goroutines total : %d (baseline %d, attributable %d)\n",
totalGoroutines, baselineGoroutines, totalGoroutines-baselineGoroutines)
fmt.Printf("goroutines / conn: %.2f\n", goroutinesPerConn)
fmt.Printf("==================================================\n\n")
if perConn > maxPerConnHeapBytes {
t.Errorf("per-conn heap %.0f B exceeds budget %d B (SCALE_STANDARD.md §3)",
perConn, maxPerConnHeapBytes)
}
if goroutinesPerConn < goroutinesPerConnLow || goroutinesPerConn > goroutinesPerConnHigh {
t.Errorf("goroutines/conn %.2f outside [%.2f, %.2f] (SCALE_STANDARD.md §3)",
goroutinesPerConn, goroutinesPerConnLow, goroutinesPerConnHigh)
}
cancel()
wg.Wait()
}
func humanBytes(n int64) string {
const unit = 1024
if n < 0 {
return "-" + humanBytes(-n)
}
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
for x := n / unit; x >= unit; x /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.2f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}