perf(inbounds): batch client identities for stats enrichment (#6929)

backfillClientStats loaded every column of every client once per inbound,
although enrichment only reads UUID, email and subscription ID. Each inbound
list read (panel list, the master's node traffic poll, Xray config builds,
bots) repeated that wide join per inbound. Identities now come from one narrow
clients/client_inbounds projection per 400 inbounds, honouring the supplied
transaction; stats output, sibling-traffic append order and the #6436
clients-table source of truth are unchanged.

Closes #6926
This commit is contained in:
Hamed
2026-10-25 16:31:46 +03:30
committed by GitHub
parent be25314157
commit 952c1c751a
11 changed files with 561 additions and 38 deletions
+9 -14
View File
@@ -20,11 +20,9 @@ import (
"gorm.io/gorm"
)
// setupScaleDB initializes the DB for a scale benchmark on either Postgres
// (XUI_DB_TYPE=postgres + XUI_DB_DSN) or SQLite (XUI_SCALE_TEST=1, temp file;
// XUI_SCALE_DB_PATH persists the DB for manual smoke runs), and registers
// cleanup. Skips the test when neither backend is configured.
func setupScaleDB(t *testing.T) {
// setupScaleDB opens package-isolated PostgreSQL or opt-in SQLite (XUI_SCALE_TEST); otherwise it skips.
// XUI_SCALE_DB_PATH preserves SQLite for manual smoke runs instead of temporary-file cleanup.
func setupScaleDB(t testing.TB) {
t.Helper()
xuilogger.InitLogger(logging.ERROR)
@@ -82,11 +80,9 @@ type scaleDataset struct {
perInbound [][]model.Client
}
// seedScaleDataset seeds n healthy clients (future expiry, unfilled quota)
// spread across numInbounds inbounds, writing inbounds, clients,
// client_inbounds and client_traffics directly in one transaction — orders of
// magnitude faster than SyncInbound and one fsync instead of thousands.
func seedScaleDataset(t *testing.T, n, numInbounds int) scaleDataset {
// seedScaleDataset writes healthy-client fixture tables directly in one transaction.
// Skipping SyncInbound keeps per-client reconciliation out of benchmark setup.
func seedScaleDataset(t testing.TB, n, numInbounds int) scaleDataset {
t.Helper()
db := database.GetDB()
resetScaleTables(t, db, "inbounds", "clients", "client_inbounds", "client_traffics")
@@ -187,10 +183,9 @@ func sampleEmails(emails []string, k int) []string {
return out
}
// resetScaleTables empties the given tables between sub-sizes. Postgres uses a
// single TRUNCATE ... CASCADE; SQLite deletes per table and clears the
// autoincrement counters so ids restart like RESTART IDENTITY.
func resetScaleTables(t *testing.T, db *gorm.DB, tables ...string) {
// resetScaleTables uses PostgreSQL TRUNCATE ... CASCADE with identity restart.
// SQLite deletes table by table, then clears its autoincrement counters.
func resetScaleTables(t testing.TB, db *gorm.DB, tables ...string) {
t.Helper()
if config.GetDBKind() == "postgres" {
stmt := "TRUNCATE TABLE " + strings.Join(tables, ", ") + " RESTART IDENTITY CASCADE"