test(sub): assert linear allocation growth instead of an absolute budget

TestSubscriptionDecodesInboundSettingsOncePerRequest capped one request
at a fixed number of bytes, which the race detector, a Go upgrade or a
richer link format can move: the race job already broke 128 MiB on
correct code, and raising the cap only waits for the next drift.

The test now builds each subscription at n and 3n clients and asserts
the allocation grows by at most 5x. One decode per request grows about
3x (2.94 to 3.40 measured, under -race too); one decode per link grows
6.9 to 9.3x, so the bound tells the two apart whatever the absolute
sizes are.
This commit is contained in:
MHSanaei
2026-11-01 14:00:29 +01:00
parent 99f386820d
commit 4dfd008edb
+22 -20
View File
@@ -2118,17 +2118,17 @@ func seedSharedSubIdInbound(t *testing.T, protocol model.Protocol, subId string,
// A subscription of n clients of one inbound must decode that inbound's settings // A subscription of n clients of one inbound must decode that inbound's settings
// once, not once per link: with the clients array inside, n decodes grow with n². // once, not once per link: with the clients array inside, n decodes grow with n².
func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) { func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) {
const n = 1000
const subId = "shared-sub-id" const subId = "shared-sub-id"
// The race detector roughly doubles allocations; one decode per link costs several times this. // Growth, not an absolute byte count, so the race detector or a Go upgrade cannot move
const maxAllocated = 256 << 20 // it: tripling n triples linear work and multiplies a per-link decode by nine.
const small, scale, maxGrowth = 300, 3, 5.0
serverPriv, serverPub := mustWireguardKeypair(t) serverPriv, serverPub := mustWireguardKeypair(t)
clientPriv, _ := mustWireguardKeypair(t) clientPriv, _ := mustWireguardKeypair(t)
email := func(i int) string { return fmt.Sprintf("user-%04d@sub", i) } email := func(i int) string { return fmt.Sprintf("user-%04d@sub", i) }
uuid := func(i int) string { return fmt.Sprintf("11111111-2222-4333-8444-%012d", i) } uuid := func(i int) string { return fmt.Sprintf("11111111-2222-4333-8444-%012d", i) }
seed := map[model.Protocol]func(t *testing.T) *model.Inbound{ seed := map[model.Protocol]func(t *testing.T, n int) *model.Inbound{
model.TUIC: func(t *testing.T) *model.Inbound { model.TUIC: func(t *testing.T, n int) *model.Inbound {
return seedSharedSubIdInbound(t, model.TUIC, subId, n, `"server":{"certificate":"/c","private_key":"/k"}`, return seedSharedSubIdInbound(t, model.TUIC, subId, n, `"server":{"certificate":"/c","private_key":"/k"}`,
func(i int) string { func(i int) string {
return fmt.Sprintf(`{"uuid":%q,"password":"pw","email":%q,"enable":true}`, uuid(i), email(i)) return fmt.Sprintf(`{"uuid":%q,"password":"pw","email":%q,"enable":true}`, uuid(i), email(i))
@@ -2137,7 +2137,7 @@ func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) {
return model.ClientRecord{Email: email(i), UUID: uuid(i), Password: "pw"} return model.ClientRecord{Email: email(i), UUID: uuid(i), Password: "pw"}
}) })
}, },
model.AmneziaWG: func(t *testing.T) *model.Inbound { model.AmneziaWG: func(t *testing.T, n int) *model.Inbound {
return seedSharedSubIdInbound(t, model.AmneziaWG, subId, n, fmt.Sprintf(`"server":{"privateKey":%q,"publicKey":%q,"mtu":1420}`, serverPriv, serverPub), return seedSharedSubIdInbound(t, model.AmneziaWG, subId, n, fmt.Sprintf(`"server":{"privateKey":%q,"publicKey":%q,"mtu":1420}`, serverPriv, serverPub),
func(i int) string { func(i int) string {
return fmt.Sprintf(`{"email":%q,"privateKey":%q,"allowedIPs":["10.8.%d.%d/32"],"enable":true}`, email(i), clientPriv, i/256, i%256) return fmt.Sprintf(`{"email":%q,"privateKey":%q,"allowedIPs":["10.8.%d.%d/32"],"enable":true}`, email(i), clientPriv, i/256, i%256)
@@ -2146,7 +2146,7 @@ func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) {
return model.ClientRecord{Email: email(i), PrivateKey: clientPriv, AllowedIPs: fmt.Sprintf("10.8.%d.%d/32", i/256, i%256)} return model.ClientRecord{Email: email(i), PrivateKey: clientPriv, AllowedIPs: fmt.Sprintf("10.8.%d.%d/32", i/256, i%256)}
}) })
}, },
model.WireGuard: func(t *testing.T) *model.Inbound { model.WireGuard: func(t *testing.T, n int) *model.Inbound {
return seedSharedSubIdInbound(t, model.WireGuard, subId, n, fmt.Sprintf(`"secretKey":%q,"mtu":1420`, serverPriv), return seedSharedSubIdInbound(t, model.WireGuard, subId, n, fmt.Sprintf(`"secretKey":%q,"mtu":1420`, serverPriv),
func(i int) string { func(i int) string {
return fmt.Sprintf(`{"email":%q,"privateKey":%q,"allowedIPs":["10.8.%d.%d/32"],"enable":true}`, email(i), clientPriv, i/256, i%256) return fmt.Sprintf(`{"email":%q,"privateKey":%q,"allowedIPs":["10.8.%d.%d/32"],"enable":true}`, email(i), clientPriv, i/256, i%256)
@@ -2155,7 +2155,7 @@ func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) {
return model.ClientRecord{Email: email(i), PrivateKey: clientPriv, AllowedIPs: fmt.Sprintf("10.8.%d.%d/32", i/256, i%256)} return model.ClientRecord{Email: email(i), PrivateKey: clientPriv, AllowedIPs: fmt.Sprintf("10.8.%d.%d/32", i/256, i%256)}
}) })
}, },
model.Hysteria: func(t *testing.T) *model.Inbound { model.Hysteria: func(t *testing.T, n int) *model.Inbound {
return seedSharedSubIdInbound(t, model.Hysteria, subId, n, `"version":2`, return seedSharedSubIdInbound(t, model.Hysteria, subId, n, `"version":2`,
func(i int) string { func(i int) string {
return fmt.Sprintf(`{"email":%q,"auth":"auth","enable":true}`, email(i)) return fmt.Sprintf(`{"email":%q,"auth":"auth","enable":true}`, email(i))
@@ -2216,19 +2216,21 @@ func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
initSubDB(t) allocated := func(n int) float64 {
seed[tc.protocol](t) initSubDB(t)
seed[tc.protocol](t, n)
var before, after runtime.MemStats var before, after runtime.MemStats
runtime.ReadMemStats(&before) runtime.ReadMemStats(&before)
got := tc.fetch(t) got := tc.fetch(t)
runtime.ReadMemStats(&after) runtime.ReadMemStats(&after)
if got != n {
if got != n { t.Fatalf("entries = %d, want %d", got, n)
t.Fatalf("entries = %d, want %d", got, n) }
return float64(after.TotalAlloc - before.TotalAlloc)
} }
if allocated := after.TotalAlloc - before.TotalAlloc; allocated > maxAllocated { base, grown := allocated(small), allocated(small*scale)
t.Fatalf("%d entries allocated %d MiB, want at most %d MiB", n, allocated>>20, maxAllocated>>20) if growth := grown / base; growth > maxGrowth {
t.Fatalf("%dx the entries allocated %.1fx the memory (%d MiB), want linear growth", scale, growth, int64(grown)>>20)
} }
}) })
} }