perf(panel): kill per-request full-database clones (item 22, backend)
Data.Clone() was a full JSON marshal/unmarshal round trip (with a per-record map dance for unknown fields): 6.3ms and ~13.8k allocs per call on sandbox hardware, and every admin GET paid it at least twice (RequireAdmin's user lookup + the handler). Clone is now a reflective deep copy of the plain data graph — byte-identical output, proven against the JSON clone on the real seed database by TestCloneEquivalence — at 0.37ms / 538 allocs. CurrentAdmin resolves the session user under a read-only store View and no longer clones at all, so an admin GET takes exactly one snapshot. Sandbox benchmarks (11 servers / 26 customers / 25 nodes): overview 17.2ms -> 0.38ms, servers 13.3 -> 0.85ms, customers 20.5 -> 1.54ms, nodes 15.8 -> 3.45ms, jobs 13.0 -> 0.39ms per request.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
// Item 22: the reflective Clone must be a perfect substitute for the
|
||||
// JSON round-trip clone — byte-identical serialization on the real
|
||||
// seed database, and fully independent of the original afterwards.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCloneEquivalenceOnSeed(t *testing.T) {
|
||||
raw, err := os.ReadFile("difftest/seed.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var d Data
|
||||
if err := unmarshalJSONUseNumber(raw, &d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fast := d.Clone()
|
||||
slow := d.cloneViaJSON()
|
||||
fastJSON, err := marshalJSON(fast)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slowJSON, err := marshalJSON(slow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(fastJSON, slowJSON) {
|
||||
t.Fatalf("clone mismatch:\nfast: %.400s\nslow: %.400s", fastJSON, slowJSON)
|
||||
}
|
||||
origJSON, err := marshalJSON(&d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(fastJSON, origJSON) {
|
||||
t.Fatal("clone must serialize identically to the source data")
|
||||
}
|
||||
// Deep independence: mutating the clone at several depths must not
|
||||
// leak into the source.
|
||||
if len(fast.Customers) > 0 {
|
||||
fast.Customers[0].Name = "MUTATED"
|
||||
if fast.Customers[0].Credentials != nil {
|
||||
fast.Customers[0].Credentials.RelayPassword = "MUTATED"
|
||||
}
|
||||
}
|
||||
if len(fast.Nodes) > 0 {
|
||||
fast.Nodes[0].Name = "MUTATED"
|
||||
}
|
||||
for key := range fast.KeyStore {
|
||||
fast.KeyStore[key].PrivateKey = "MUTATED"
|
||||
break
|
||||
}
|
||||
afterJSON, err := marshalJSON(&d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(afterJSON, origJSON) {
|
||||
t.Fatal("clone mutation leaked into the source data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneEquivalenceSynthetic(t *testing.T) {
|
||||
d := perfSeedData()
|
||||
// Add the shapes the perf seed lacks: activity detail with nested
|
||||
// generic values, keyStore entries, unknown (Extra) fields.
|
||||
d.Activity = []*Activity{{ID: "act_1", At: NowISO(), Actor: "admin", Action: "x", Target: "y",
|
||||
Detail: map[string]any{"n": json.Number("42"), "nested": map[string]any{"list": []any{"a", json.Number("1.5"), true, nil}}}}}
|
||||
d.KeyStore["reality-nod_0"] = &KeyEntry{PrivateKey: "priv", PublicKey: "pub", UpdatedAt: NowISO()}
|
||||
fastJSON, err := marshalJSON(d.Clone())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slowJSON, err := marshalJSON(d.cloneViaJSON())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(fastJSON, slowJSON) {
|
||||
t.Fatalf("synthetic clone mismatch:\nfast: %.300s\nslow: %.300s", fastJSON, slowJSON)
|
||||
}
|
||||
if d.Clone() == nil || EmptyData().Clone() == nil {
|
||||
t.Fatal("clone of empty data must be non-nil")
|
||||
}
|
||||
var nilData *Data
|
||||
if nilData.Clone() != nil {
|
||||
t.Fatal("clone of nil must be nil")
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -848,19 +848,29 @@ type adminAuth struct {
|
||||
User *User
|
||||
}
|
||||
|
||||
// CurrentAdmin is currentAdmin.
|
||||
// CurrentAdmin is currentAdmin. Since round-3 item 22 it resolves the
|
||||
// session's user under a read-only View instead of cloning the whole
|
||||
// database: admin auth runs on EVERY request, and the user table is
|
||||
// tiny. The returned User is a copy the caller may read freely.
|
||||
func (a *App) CurrentAdmin(r *http.Request) *adminAuth {
|
||||
session := a.getSessions().Get(r)
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
data := a.store.Snapshot()
|
||||
for _, user := range data.Users {
|
||||
var found *User
|
||||
a.store.View(func(d *Data) {
|
||||
for _, user := range d.Users {
|
||||
if user.ID == session.UserID && user.Status == "active" {
|
||||
return &adminAuth{Session: session, User: user}
|
||||
copied := *user
|
||||
found = &copied
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if found == nil {
|
||||
return nil
|
||||
}
|
||||
return &adminAuth{Session: session, User: found}
|
||||
}
|
||||
|
||||
// RequireAdmin is requireAdmin: sends 401/403 itself and returns nil.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
// Benchmarks for round-3 item 22 (per-request full-clone cost). Not run
|
||||
// by `go test` without -bench. The seed is synthetic data at production
|
||||
// scale (~11 servers / 26 customers / 25 nodes / ~100 bindings).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func perfSeedData() *Data {
|
||||
d := EmptyData()
|
||||
d.Users = []*User{{ID: "usr_1", Username: "admin", PasswordHash: "x", Role: "owner", Status: "active", CreatedAt: NowISO()}}
|
||||
for i := 0; i < 11; i++ {
|
||||
d.Servers = append(d.Servers, &Server{
|
||||
ID: fmt.Sprintf("srv_%d", i), Name: fmt.Sprintf("S%d", i), PublicAddress: "203.0.113.1",
|
||||
Status: "online", LastSeenAt: StrPtr(NowISO()), PortRangeStart: 20000, PortRangeEnd: 50000,
|
||||
Labels: []string{}, CreatedAt: NowISO(), UpdatedAt: NowISO(),
|
||||
})
|
||||
}
|
||||
for i := 0; i < 26; i++ {
|
||||
d.Customers = append(d.Customers, &Customer{
|
||||
ID: fmt.Sprintf("cus_%d", i), Name: fmt.Sprintf("C%d", i), Status: "active",
|
||||
SubscriptionToken: fmt.Sprintf("tok-%032d", i),
|
||||
Credentials: &Credentials{ClientID: NewUUID(), RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001"},
|
||||
Tags: []string{}, CreatedAt: NowISO(), UpdatedAt: NowISO(),
|
||||
})
|
||||
}
|
||||
protocols := []string{"vless-reality-vision", "shadowsocks-2022-aes128", "socks5-auth", "vless-ws", "hysteria2"}
|
||||
for i := 0; i < 25; i++ {
|
||||
node := &Node{
|
||||
ID: fmt.Sprintf("nod_%d", i), Name: fmt.Sprintf("N%d", i), ServerID: fmt.Sprintf("srv_%d", i%11),
|
||||
Protocol: protocols[i%len(protocols)], Topology: "direct", NetworkMode: "ipv4", Port: 23000 + i,
|
||||
Status: "active", ResourceID: fmt.Sprintf("res_%d", i), Artifacts: map[string]json.RawMessage{},
|
||||
}
|
||||
if node.Protocol == "socks5-auth" {
|
||||
node.SocksUser, node.SocksPassword = "ngs_fixed", "fixedpass"
|
||||
}
|
||||
if IsRealityProtocol(node.Protocol) {
|
||||
node.Reality = &RealityConfig{KeyID: fmt.Sprintf("reality-nod_%d", i), ServerName: "www.tesla.com", DestPort: 443}
|
||||
}
|
||||
d.Nodes = append(d.Nodes, node)
|
||||
for j := 0; j < 4; j++ {
|
||||
d.NodeClients = append(d.NodeClients, &NodeClient{
|
||||
ID: fmt.Sprintf("nc_%d_%d", i, j), NodeID: node.ID,
|
||||
CustomerID: fmt.Sprintf("cus_%d", (i+j)%26), Status: "active",
|
||||
CreatedAt: NowISO(), UpdatedAt: NowISO(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func BenchmarkCloneJSONRoundTrip(b *testing.B) {
|
||||
d := perfSeedData()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = d.cloneViaJSON()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCloneFast(b *testing.B) {
|
||||
d := perfSeedData()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = d.Clone()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAdminGET(b *testing.B) {
|
||||
cookieSecure = false
|
||||
store := NewStore(filepath.Join(b.TempDir(), "nexusgate.json"))
|
||||
if err := store.Init(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
seed := perfSeedData()
|
||||
seed.Users[0].PasswordHash = HashSecret("password12345")
|
||||
if _, err := store.Transaction(func(d *Data) (any, error) { *d = *seed; return nil, nil }); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
app := NewApp(store, NewSessionManager(12), NewOrchestrator(store))
|
||||
login := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"username":"admin","password":"password12345"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
app.ServeHTTP(rec, login)
|
||||
if rec.Code != 200 {
|
||||
b.Fatalf("login %d", rec.Code)
|
||||
}
|
||||
cookie := strings.Split(rec.Header().Get("Set-Cookie"), ";")[0]
|
||||
for _, path := range []string{"/api/overview", "/api/servers", "/api/customers", "/api/nodes", "/api/jobs"} {
|
||||
b.Run(strings.TrimPrefix(path, "/api/"), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
req.Header.Set("Cookie", cookie)
|
||||
rec := httptest.NewRecorder()
|
||||
app.ServeHTTP(rec, req)
|
||||
if rec.Code != 200 {
|
||||
b.Fatalf("%s -> %d", path, rec.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -118,9 +118,15 @@ func NodeClientURI(d *Data, nc *NodeClient) string {
|
||||
socksUser := NodeEmail(nc.CustomerID)
|
||||
relayPassword := creds.RelayPassword
|
||||
if node.Protocol == "socks5-auth" {
|
||||
EnsureSocksCredentials(node)
|
||||
socksUser = node.SocksUser
|
||||
relayPassword = node.SocksPassword
|
||||
socksUser, relayPassword = node.SocksUser, node.SocksPassword
|
||||
if socksUser == "" || relayPassword == "" {
|
||||
// Legacy record the load migration has not reached (only
|
||||
// possible on an in-memory fixture): mint into a throwaway
|
||||
// copy — a read path must never mutate the node record.
|
||||
probe := *node
|
||||
EnsureSocksCredentials(&probe)
|
||||
socksUser, relayPassword = probe.SocksUser, probe.SocksPassword
|
||||
}
|
||||
}
|
||||
uri, ok := BuildClientURI(BuildClientURIParams{
|
||||
Protocol: node.Protocol,
|
||||
|
||||
+78
-1
@@ -254,8 +254,85 @@ func (d Data) MarshalJSON() ([]byte, error) {
|
||||
return mergeMarshal(plain(d), d.Extra)
|
||||
}
|
||||
|
||||
// Clone is structuredClone(this.data).
|
||||
// Clone is structuredClone(this.data). Since round-3 item 22 it is a
|
||||
// reflective deep copy: the old JSON marshal/unmarshal round trip cost
|
||||
// a full serialize + parse of the whole database (with a per-record
|
||||
// map dance for unknown fields) on EVERY store read — twice per admin
|
||||
// request — and dominated panel latency on the router's CPU. The data
|
||||
// graph is plain exported structs/slices/maps/strings/numbers, which
|
||||
// reflection copies exactly; TestCloneEquivalence proves byte-identical
|
||||
// output against cloneViaJSON on the real seed database.
|
||||
func (d *Data) Clone() *Data {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
return cloneReflectValue(reflect.ValueOf(d)).Interface().(*Data)
|
||||
}
|
||||
|
||||
// cloneReflectValue deep-copies v. Structs are shallow-copied first (so
|
||||
// any unexported field keeps its value) and every exported field is
|
||||
// then replaced by its deep copy; []byte (incl. json.RawMessage) is
|
||||
// copied verbatim; scalars pass through.
|
||||
func cloneReflectValue(v reflect.Value) reflect.Value {
|
||||
if !v.IsValid() {
|
||||
return v
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
return v
|
||||
}
|
||||
out := reflect.New(v.Type().Elem())
|
||||
out.Elem().Set(cloneReflectValue(v.Elem()))
|
||||
return out
|
||||
case reflect.Interface:
|
||||
if v.IsNil() {
|
||||
return v
|
||||
}
|
||||
return reflect.ValueOf(cloneReflectValue(v.Elem()).Interface())
|
||||
case reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return v
|
||||
}
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
out := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
|
||||
reflect.Copy(out, v)
|
||||
return out
|
||||
}
|
||||
out := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
out.Index(i).Set(cloneReflectValue(v.Index(i)))
|
||||
}
|
||||
return out
|
||||
case reflect.Map:
|
||||
if v.IsNil() {
|
||||
return v
|
||||
}
|
||||
out := reflect.MakeMapWithSize(v.Type(), v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
out.SetMapIndex(cloneReflectValue(iter.Key()), cloneReflectValue(iter.Value()))
|
||||
}
|
||||
return out
|
||||
case reflect.Struct:
|
||||
out := reflect.New(v.Type()).Elem()
|
||||
out.Set(v)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if v.Type().Field(i).PkgPath != "" {
|
||||
continue // unexported: kept from the shallow copy
|
||||
}
|
||||
out.Field(i).Set(cloneReflectValue(v.Field(i)))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// cloneViaJSON is the original Clone implementation: a full JSON
|
||||
// marshal/unmarshal round trip. Kept for the equivalence test and the
|
||||
// item-22 benchmarks that quantify what the reflective Clone replaced.
|
||||
func (d *Data) cloneViaJSON() *Data {
|
||||
raw, err := marshalJSON(d)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
Reference in New Issue
Block a user