mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-08 21:56:08 +00:00
4fc301682f
The paths that run continuously in production had no scale coverage: the 5s traffic poll (AddTraffic with its auto-renew and depleted scans), the websocket snapshot the job broadcasts while a browser is connected, the 10s ip-limit job (hasLimitIp LIKE scan + per-email settings parse), a subscription fetch inside a huge inbound, and the full Xray config build. New benchmarks reuse the XUI_SCALE_TEST / XUI_DB_TYPE gating and stay log-only. Sizes default to 10k/100k; XUI_SCALE_SIZES=500000 raises the ladder without editing code. seedScaleDataset writes inbounds, clients, client_inbounds and client_traffics directly in one transaction instead of SyncInbound, so a 500k seed takes seconds. XUI_SCALE_DB_PATH persists the seeded SQLite file for manual smoke runs against a live panel.
107 lines
3.7 KiB
Go
107 lines
3.7 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
func pollReport(ds scaleDataset, k int) ([]*xray.Traffic, []*xray.ClientTraffic) {
|
|
traffics := make([]*xray.Traffic, 0, len(ds.tags))
|
|
for _, tag := range ds.tags {
|
|
traffics = append(traffics, &xray.Traffic{IsInbound: true, Tag: tag, Up: 1 << 20, Down: 2 << 20})
|
|
}
|
|
clientTraffics := make([]*xray.ClientTraffic, 0, k)
|
|
for _, email := range sampleEmails(ds.emails, k) {
|
|
clientTraffics = append(clientTraffics, &xray.ClientTraffic{Email: email, Up: 1 << 20, Down: 2 << 20})
|
|
}
|
|
return traffics, clientTraffics
|
|
}
|
|
|
|
// TestAddTrafficPollScale measures one full traffic-poll cycle (the @every 5s
|
|
// job): per-client delta UPDATEs, the auto-renew probe and the depleted-client
|
|
// scan, in steady state and with 100 clients depleting / renewing.
|
|
func TestAddTrafficPollScale(t *testing.T) {
|
|
setupScaleDB(t)
|
|
svc := &InboundService{}
|
|
shapes := []struct {
|
|
name string
|
|
inbounds int
|
|
}{{"single", 1}, {"spread50", 50}}
|
|
|
|
for _, n := range scaleSizes(t, 10000, 100000) {
|
|
for _, shape := range shapes {
|
|
t.Run(fmt.Sprintf("N=%d_%s", n, shape.name), func(t *testing.T) {
|
|
db := database.GetDB()
|
|
ds := seedScaleDataset(t, n, shape.inbounds)
|
|
|
|
for _, k := range []int{1000, 10000} {
|
|
if k > n {
|
|
continue
|
|
}
|
|
traffics, clientTraffics := pollReport(ds, k)
|
|
const reps = 3
|
|
start := time.Now()
|
|
for range reps {
|
|
if _, _, err := svc.AddTraffic(traffics, clientTraffics); err != nil {
|
|
t.Fatalf("AddTraffic steady: %v", err)
|
|
}
|
|
}
|
|
perPoll := time.Since(start) / reps
|
|
t.Logf("N=%-7d shape=%-8s K=%-6d steady=%v/poll", n, shape.name, k, perPoll.Round(time.Millisecond))
|
|
|
|
var probe xray.ClientTraffic
|
|
if err := db.Where("email = ?", clientTraffics[0].Email).First(&probe).Error; err != nil {
|
|
t.Fatalf("load probe row: %v", err)
|
|
}
|
|
if probe.Up == 0 || probe.Down == 0 {
|
|
t.Fatalf("steady polls did not accumulate traffic: up=%d down=%d", probe.Up, probe.Down)
|
|
}
|
|
}
|
|
|
|
depleted := ds.perInbound[0][:100]
|
|
if err := db.Model(&xray.ClientTraffic{}).
|
|
Where("email IN ?", emailsOf(depleted)).
|
|
Updates(map[string]any{"up": int64(100 << 30), "down": int64(0)}).Error; err != nil {
|
|
t.Fatalf("mark depleted: %v", err)
|
|
}
|
|
start := time.Now()
|
|
if _, _, err := svc.AddTraffic(nil, nil); err != nil {
|
|
t.Fatalf("AddTraffic disable: %v", err)
|
|
}
|
|
t.Logf("N=%-7d shape=%-8s disable100=%v", n, shape.name, time.Since(start).Round(time.Millisecond))
|
|
var disabledCount int64
|
|
if err := db.Model(&xray.ClientTraffic{}).Where("enable = ?", false).Count(&disabledCount).Error; err != nil {
|
|
t.Fatalf("count disabled: %v", err)
|
|
}
|
|
if disabledCount != 100 {
|
|
t.Fatalf("disable100: got %d disabled rows, want 100", disabledCount)
|
|
}
|
|
|
|
renew := ds.perInbound[0][100:200]
|
|
past := time.Now().Add(-time.Hour).UnixMilli()
|
|
if err := db.Model(&xray.ClientTraffic{}).
|
|
Where("email IN ?", emailsOf(renew)).
|
|
Updates(map[string]any{"reset": 30, "expiry_time": past, "up": int64(1 << 30)}).Error; err != nil {
|
|
t.Fatalf("mark renewable: %v", err)
|
|
}
|
|
start = time.Now()
|
|
if _, _, err := svc.AddTraffic(nil, nil); err != nil {
|
|
t.Fatalf("AddTraffic renew: %v", err)
|
|
}
|
|
t.Logf("N=%-7d shape=%-8s renew100=%v", n, shape.name, time.Since(start).Round(time.Millisecond))
|
|
var renewed xray.ClientTraffic
|
|
if err := db.Where("email = ?", renew[0].Email).First(&renewed).Error; err != nil {
|
|
t.Fatalf("load renewed row: %v", err)
|
|
}
|
|
if renewed.ExpiryTime <= past || renewed.Up != 0 {
|
|
t.Fatalf("renew100 did not renew: expiry=%d up=%d", renewed.ExpiryTime, renewed.Up)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|