mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
zip is shared protocol infra (the fiber-over-ZAP web framework used by every
Lux/Hanzo/Zoo Go binary — nodes, gateway, cloud, iam, kms, o11y, ai, …), so its
home is the ZAP protocol family (zap-proto/{go,http,zapd,zip}), NOT any one org's
brand namespace. This consolidates the rich framework (formerly hanzoai/zip v0.5.0
— Ctx/Handler/Errorf/Err*/HTTPError, typed handlers, middleware, runtime/extension
loading, openapi, validate, zaprpc) with the real ZAP transport (ListenZAP/
ListenHTTP/Serve over zap-proto/http) into ONE canonical module.
Module path github.com/hanzoai/zip -> github.com/zap-proto/zip. Code identical to
hanzoai/zip v0.5.0; promoted to v1.0.0 as the stable shared-infra framework. The
prior hanzoai/zip lineage is retired — one home, one import path, one way.
Verified: go build/vet/test ./... green; the ZAP round-trip integration test serves
/v1/health over the real ZAP transport (200 + handler JSON).
167 lines
4.4 KiB
Go
167 lines
4.4 KiB
Go
// Copyright © 2026 Hanzo AI. MIT License.
|
|
|
|
// Per-connection memory profile for the zip framework itself —
|
|
// canonical regression test cited by SCALE_STANDARD.md §3 and §8.
|
|
// Every Hanzo Go service that exposes an HTTP listener copies this
|
|
// file into its own repo and tunes the route registration to the
|
|
// service shape; the budget assertion stays the same.
|
|
//
|
|
// Budget (verified on Apple M1 Max / Go 1.26.3 / Fiber v3 v3.2.0):
|
|
// per_conn_heap <= 12 KiB (target: 8 KiB)
|
|
// goroutines_per_conn == 1.00 (tolerance ±0.05)
|
|
//
|
|
// Run with:
|
|
// go test -mod=mod -run=TestConnMemory -v -conn-count=10000
|
|
|
|
package zip_test
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"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")
|
|
|
|
// Budget assertions, expressed in bytes / ratio. SCALE_STANDARD.md §8
|
|
// pins these — do not loosen them without updating the doc.
|
|
const (
|
|
maxPerConnHeapBytes = 12 * 1024 // 12 KiB ceiling
|
|
goroutinesPerConnLow = 0.95
|
|
goroutinesPerConnHigh = 1.05
|
|
settleBeforeBaselineMs = 200
|
|
maxWaitForAcceptSeconds = 30
|
|
)
|
|
|
|
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", "zip-conn-memory"),
|
|
DisableStartupMessage: true,
|
|
})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
var holding atomic.Int64
|
|
app.Get("/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(settleBeforeBaselineMs * time.Millisecond)
|
|
var baseline, peak runtime.MemStats
|
|
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(maxWaitForAcceptSeconds * 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 — listener never came up")
|
|
}
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&peak)
|
|
|
|
delta := int64(peak.HeapAlloc) - int64(baseline.HeapAlloc)
|
|
perConn := float64(delta) / float64(n)
|
|
totalGoroutines := runtime.NumGoroutine()
|
|
goroutinesPerConn := float64(totalGoroutines) / float64(n)
|
|
|
|
fmt.Printf("\n=== Per-connection memory profile (zip) ===\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\n", totalGoroutines)
|
|
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 (see SCALE_STANDARD.md §3)",
|
|
perConn, maxPerConnHeapBytes)
|
|
}
|
|
if goroutinesPerConn < goroutinesPerConnLow || goroutinesPerConn > goroutinesPerConnHigh {
|
|
t.Errorf("goroutines/conn %.2f outside [%.2f, %.2f] (see 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])
|
|
}
|