perf: optimize throughput and scheduler efficency

This commit is contained in:
xboard
2026-03-23 14:46:43 +08:00
parent 4f1f41aa5d
commit e8e2abbc37
4 changed files with 135 additions and 46 deletions
+12
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"sort"
"sync"
"github.com/cedar2025/xboard-node/internal/panel"
"golang.org/x/time/rate"
@@ -88,6 +89,17 @@ type Connection struct {
SourceIP string // client source IP (cleaned, no port/brackets)
}
var ConnectionSlicePool = sync.Pool{
New: func() any { return make([]Connection, 0, 256) },
}
func ReleaseConnectionSlice(s []Connection) {
if s == nil {
return
}
ConnectionSlicePool.Put(s[:0])
}
// UserDiff computes which users to add and which to remove when transitioning
// from oldUsers to newUsers. This is a pure helper used by callers; kernels
// may also use it internally.
+102 -45
View File
@@ -8,6 +8,7 @@ import (
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/buf"
@@ -18,6 +19,8 @@ import (
"github.com/cedar2025/xboard-node/internal/kernel"
)
const rateLimitThreshold = 64 * 1024
// connRecord holds the minimal per-connection state we need.
// Each field is accessed by multiple goroutines; upload/download use atomics.
type connRecord struct {
@@ -201,19 +204,22 @@ func (t *ConnTracker) Snapshot() []kernel.Connection {
// ~8 bytes vs ~80 bytes for a full Connection struct, and no atomic loads.
// Phase 2 (lock-free): call toConnection() which atomically reads the
// upload/download counters. Atomics are safe without any mutex.
// This shrinks the RLock window proportionally as connection count grows,
// reducing serialisation against concurrent allocRecord/closeRecord calls.
t.mu.RLock()
ptrs := make([]*connRecord, 0, len(t.active))
n := len(t.active)
t.mu.RUnlock()
result := kernel.ConnectionSlicePool.Get().([]kernel.Connection)
result = result[:0]
if cap(result) < n+len(pending) {
result = make([]kernel.Connection, 0, n+len(pending))
}
t.mu.RLock()
for _, rec := range t.active {
ptrs = append(ptrs, rec)
result = append(result, rec.toConnection())
}
t.mu.RUnlock()
result := make([]kernel.Connection, 0, len(ptrs)+len(pending))
for _, rec := range ptrs {
result = append(result, rec.toConnection())
}
for _, rec := range pending {
result = append(result, rec.toConnection())
}
@@ -246,10 +252,11 @@ func (t *ConnTracker) ActiveCount() int {
type trackedConn struct {
net.Conn
rec *connRecord
tracker *ConnTracker
limiter *rate.Limiter // nil = unlimited
ctx context.Context // for rate limiter WaitN cancellation
rec *connRecord
tracker *ConnTracker
limiter *rate.Limiter // nil = unlimited
ctx context.Context // for rate limiter WaitN cancellation
pendingTokens atomic.Int64 // amortized token accumulator for CountFunc
}
// Read is the fallback path used when the copy pipeline cannot unwrap this
@@ -264,8 +271,16 @@ func (c *trackedConn) Read(b []byte) (int, error) {
if n > 0 {
c.rec.download.Add(int64(n))
if c.limiter != nil {
waitN := min(n, c.limiter.Burst())
_ = c.limiter.WaitN(c.ctx, waitN)
if pending := c.pendingTokens.Add(int64(n)); pending >= rateLimitThreshold {
tokens := int(c.pendingTokens.Swap(0))
if burst := c.limiter.Burst(); tokens > burst {
tokens = burst
}
resv := c.limiter.ReserveN(time.Now(), tokens)
if delay := resv.Delay(); delay > 0 {
time.Sleep(delay)
}
}
}
}
return n, err
@@ -273,24 +288,21 @@ func (c *trackedConn) Read(b []byte) (int, error) {
// Write is the fallback path.
func (c *trackedConn) Write(b []byte) (int, error) {
if c.limiter != nil {
burst := c.limiter.Burst()
var n int
for n < len(b) {
chunk := min(len(b)-n, burst)
_ = c.limiter.WaitN(c.ctx, chunk)
written, writeErr := c.Conn.Write(b[n : n+chunk])
n += written
c.rec.upload.Add(int64(written))
if writeErr != nil {
return n, writeErr
}
}
return n, nil
}
n, err := c.Conn.Write(b)
if n > 0 {
c.rec.upload.Add(int64(n))
if c.limiter != nil {
if pending := c.pendingTokens.Add(int64(n)); pending >= rateLimitThreshold {
tokens := int(c.pendingTokens.Swap(0))
if burst := c.limiter.Burst(); tokens > burst {
tokens = burst
}
resv := c.limiter.ReserveN(time.Now(), tokens)
if delay := resv.Delay(); delay > 0 {
time.Sleep(delay)
}
}
}
}
return n, err
}
@@ -303,6 +315,11 @@ func (c *trackedConn) Close() error {
// makeCountFunc builds a CountFunc that does byte accounting and optional
// rate limiting in a single callback. The direction is selected by the
// counter pointer (download or upload).
//
// For rate-limited connections, tokens are accumulated in pendingTokens and
// only consumed (via WaitN) when the threshold is reached. This amortizes
// goroutine blocking from hundreds-of-thousands per second to ~10 per second
// per connection, eliminating scheduler thrashing under high concurrency.
func (c *trackedConn) makeCountFunc(counter *atomic.Int64) N.CountFunc {
if c.limiter == nil {
// Fast path: no rate limiting, just atomic add.
@@ -310,11 +327,23 @@ func (c *trackedConn) makeCountFunc(counter *atomic.Int64) N.CountFunc {
}
return func(n int64) {
counter.Add(n)
waitN := int(n)
if burst := c.limiter.Burst(); waitN > burst {
waitN = burst
if pending := c.pendingTokens.Add(n); pending >= rateLimitThreshold {
// Drain all accumulated tokens in one blocking call.
tokens := int(c.pendingTokens.Swap(0))
if burst := c.limiter.Burst(); tokens > burst {
tokens = burst
}
resv := c.limiter.ReserveN(time.Now(), tokens)
if delay := resv.Delay(); delay > 0 {
t := time.NewTimer(delay)
select {
case <-t.C:
case <-c.ctx.Done():
t.Stop()
resv.Cancel()
}
}
}
_ = c.limiter.WaitN(c.ctx, waitN)
}
}
@@ -338,10 +367,11 @@ func (c *trackedConn) WriterReplaceable() bool { return true }
type trackedPacketConn struct {
N.PacketConn
rec *connRecord
tracker *ConnTracker
limiter *rate.Limiter
ctx context.Context
rec *connRecord
tracker *ConnTracker
limiter *rate.Limiter
ctx context.Context
pendingTokens atomic.Int64
}
func (c *trackedPacketConn) ReadPacket(buffer *buf.Buffer) (singM.Socksaddr, error) {
@@ -350,8 +380,16 @@ func (c *trackedPacketConn) ReadPacket(buffer *buf.Buffer) (singM.Socksaddr, err
n := int64(buffer.Len())
c.rec.download.Add(n)
if c.limiter != nil {
waitN := int(min(n, int64(c.limiter.Burst())))
_ = c.limiter.WaitN(c.ctx, waitN)
if pending := c.pendingTokens.Add(n); pending >= rateLimitThreshold {
tokens := int(c.pendingTokens.Swap(0))
if burst := c.limiter.Burst(); tokens > burst {
tokens = burst
}
resv := c.limiter.ReserveN(time.Now(), tokens)
if delay := resv.Delay(); delay > 0 {
time.Sleep(delay)
}
}
}
}
return dest, err
@@ -363,8 +401,16 @@ func (c *trackedPacketConn) WritePacket(buffer *buf.Buffer, dest singM.Socksaddr
if err == nil {
c.rec.upload.Add(n)
if c.limiter != nil {
waitN := int(min(n, int64(c.limiter.Burst())))
_ = c.limiter.WaitN(c.ctx, waitN)
if pending := c.pendingTokens.Add(n); pending >= rateLimitThreshold {
tokens := int(c.pendingTokens.Swap(0))
if burst := c.limiter.Burst(); tokens > burst {
tokens = burst
}
resv := c.limiter.ReserveN(time.Now(), tokens)
if delay := resv.Delay(); delay > 0 {
time.Sleep(delay)
}
}
}
}
return err
@@ -381,11 +427,22 @@ func (c *trackedPacketConn) makeCountFunc(counter *atomic.Int64) N.CountFunc {
}
return func(n int64) {
counter.Add(n)
waitN := int(n)
if burst := c.limiter.Burst(); waitN > burst {
waitN = burst
if pending := c.pendingTokens.Add(n); pending >= rateLimitThreshold {
tokens := int(c.pendingTokens.Swap(0))
if burst := c.limiter.Burst(); tokens > burst {
tokens = burst
}
resv := c.limiter.ReserveN(time.Now(), tokens)
if delay := resv.Delay(); delay > 0 {
t := time.NewTimer(delay)
select {
case <-t.C:
case <-c.ctx.Done():
t.Stop()
resv.Cancel()
}
}
}
_ = c.limiter.WaitN(c.ctx, waitN)
}
}