perf(sub): decode TUIC and AmneziaWG settings once per request

genTuicLink and genAmneziaWGLink, and their Clash and JSON siblings,
parsed the inbound's whole settings, every client included, for each
link they built, bypassing the per-request caches the other generators
use. Building the links of n clients of one inbound, as the export of
all links does, cost n full parses of an n-client document.

Typed per-request caches next to settingsByInbound now hold the decoded
TUIC instance, indexed by email, and the AmneziaWG server block, and
every generator reads through them.
This commit is contained in:
MHSanaei
2026-11-01 09:09:28 +01:00
parent dda44c1e72
commit 7163c6e521
5 changed files with 244 additions and 50 deletions
+153
View File
@@ -6,6 +6,7 @@ import (
"net"
"net/url"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -2077,3 +2078,155 @@ func TestSub_LegacyFallbackMasterLookedUpOncePerRequest(t *testing.T) {
t.Fatalf("child-1 must carry its front's TLS state: %s", links[0])
}
}
// seedSharedSubIdInbound stores one inbound whose n clients all carry subId, the
// shape a node merge produces (the admin API keeps a subId unique per client).
func seedSharedSubIdInbound(t *testing.T, protocol model.Protocol, subId string, n int, serverJSON string, clientJSON func(i int) string, record func(i int) model.ClientRecord) *model.Inbound {
t.Helper()
entries := make([]string, n)
records := make([]model.ClientRecord, n)
for i := range entries {
entries[i] = clientJSON(i)
records[i] = record(i)
records[i].SubID = subId
records[i].Enable = true
}
db := database.GetDB()
ib := &model.Inbound{
UserId: 1, Tag: string(protocol), Enable: true, Listen: "203.0.113.5", Port: 443, Protocol: protocol,
Remark: string(protocol),
Settings: fmt.Sprintf(`{%s,"clients":[%s]}`, serverJSON, strings.Join(entries, ",")),
}
if err := db.Create(ib).Error; err != nil {
t.Fatalf("create %s inbound: %v", protocol, err)
}
if err := db.CreateInBatches(&records, 100).Error; err != nil {
t.Fatalf("create clients: %v", err)
}
links := make([]model.ClientInbound, n)
for i := range records {
links[i] = model.ClientInbound{ClientId: records[i].Id, InboundId: ib.Id}
}
if err := db.CreateInBatches(&links, 500).Error; err != nil {
t.Fatalf("link clients: %v", err)
}
return ib
}
// 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².
func TestSubscriptionDecodesInboundSettingsOncePerRequest(t *testing.T) {
const n = 1000
const subId = "shared-sub-id"
const maxAllocated = 128 << 20
serverPriv, serverPub := mustWireguardKeypair(t)
clientPriv, _ := mustWireguardKeypair(t)
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) }
seed := map[model.Protocol]func(t *testing.T) *model.Inbound{
model.TUIC: func(t *testing.T) *model.Inbound {
return seedSharedSubIdInbound(t, model.TUIC, subId, n, `"server":{"certificate":"/c","private_key":"/k"}`,
func(i int) string {
return fmt.Sprintf(`{"uuid":%q,"password":"pw","email":%q,"enable":true}`, uuid(i), email(i))
},
func(i int) model.ClientRecord {
return model.ClientRecord{Email: email(i), UUID: uuid(i), Password: "pw"}
})
},
model.AmneziaWG: func(t *testing.T) *model.Inbound {
return seedSharedSubIdInbound(t, model.AmneziaWG, subId, n, fmt.Sprintf(`"server":{"privateKey":%q,"publicKey":%q,"mtu":1420}`, serverPriv, serverPub),
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)
},
func(i int) model.ClientRecord {
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 {
return seedSharedSubIdInbound(t, model.WireGuard, subId, n, fmt.Sprintf(`"secretKey":%q,"mtu":1420`, serverPriv),
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)
},
func(i int) model.ClientRecord {
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 {
return seedSharedSubIdInbound(t, model.Hysteria, subId, n, `"version":2`,
func(i int) string {
return fmt.Sprintf(`{"email":%q,"auth":"auth","enable":true}`, email(i))
},
func(i int) model.ClientRecord { return model.ClientRecord{Email: email(i), Auth: "auth"} })
},
}
raw := func(t *testing.T) int {
links, _, _, _, err := NewSubService("").GetSubs(subId, "sub.example.com")
if err != nil {
t.Fatalf("GetSubs: %v", err)
}
for _, link := range links {
if link == "" {
t.Fatal("GetSubs returned an empty link")
}
}
return len(links)
}
clash := func(t *testing.T) int {
out, _, err := NewSubClashService(false, "", false, NewSubService("")).GetClash(subId, "sub.example.com")
if err != nil {
t.Fatalf("GetClash: %v", err)
}
var doc struct {
Proxies []map[string]any `yaml:"proxies"`
}
if err := yaml.Unmarshal([]byte(out), &doc); err != nil {
t.Fatalf("parse clash yaml: %v", err)
}
return len(doc.Proxies)
}
jsonSub := func(t *testing.T) int {
out, _, err := NewSubJsonService("", "", "", "", NewSubService("")).GetJson(subId, "sub.example.com", true)
if err != nil {
t.Fatalf("GetJson: %v", err)
}
var configs []json.RawMessage
if err := json.Unmarshal([]byte(out), &configs); err != nil {
t.Fatalf("parse json subscription: %v", err)
}
return len(configs)
}
cases := []struct {
name string
protocol model.Protocol
fetch func(t *testing.T) int
}{
{"raw tuic", model.TUIC, raw},
{"raw amneziawg", model.AmneziaWG, raw},
{"clash tuic", model.TUIC, clash},
{"clash amneziawg", model.AmneziaWG, clash},
{"clash wireguard", model.WireGuard, clash},
{"json wireguard", model.WireGuard, jsonSub},
{"json hysteria", model.Hysteria, jsonSub},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
initSubDB(t)
seed[tc.protocol](t)
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
got := tc.fetch(t)
runtime.ReadMemStats(&after)
if got != n {
t.Fatalf("entries = %d, want %d", got, n)
}
if allocated := after.TotalAlloc - before.TotalAlloc; allocated > maxAllocated {
t.Fatalf("%d entries allocated %d MiB, want at most %d MiB", n, allocated>>20, maxAllocated>>20)
}
})
}
}