Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"net"
"net/url"
"strings"
"sync"
"time"

"github.com/metacubex/mihomo/common/atomic"
Expand All @@ -23,7 +25,11 @@ import (
var UnifiedDelay = atomic.NewBool(false)

const (
defaultHistoriesNum = 10
defaultHistoriesNum = 10
defaultSpeedHistoriesNum = 120
defaultTestResultsNum = 20
speedStaleThreshold = 30 * time.Second
speedDecayLambda = 0.001
)

type internalProxyState struct {
Expand All @@ -36,6 +42,22 @@ type Proxy struct {
alive atomic.Bool
history *queue.Queue[C.DelayHistory]
extra xsync.Map[string, *internalProxyState]

peakSpeed atomic.Uint64
peakAt atomic.Int64
currentSpeed atomic.Uint64
currentAt atomic.Int64

speedHistoryMu sync.Mutex
speedHistory []C.SpeedHistory

testResultsMu sync.Mutex
testResults []TestResult
}

type TestResult struct {
At time.Time
Success bool
}

// Adapter implements C.Proxy
Expand Down Expand Up @@ -286,6 +308,85 @@ func NewProxy(adapter C.ProxyAdapter) *Proxy {
}
}

// PushSpeed implements C.Proxy - updates current speed and peak if exceeds decayed peak
func (p *Proxy) PushSpeed(speed uint64) {
if speed == 0 {
return
}
now := time.Now()
p.currentSpeed.Store(speed)
p.currentAt.Store(now.UnixNano())

peak := p.peakSpeed.Load()
peakAt := time.Unix(0, p.peakAt.Load())
decayedPeak := uint64(float64(peak) * math.Exp(-speedDecayLambda*now.Sub(peakAt).Seconds()))
if speed >= decayedPeak {
p.peakSpeed.Store(speed)
p.peakAt.Store(now.UnixNano())
}

p.speedHistoryMu.Lock()
p.speedHistory = append(p.speedHistory, C.SpeedHistory{Time: now, Speed: speed})
if len(p.speedHistory) > defaultSpeedHistoriesNum {
p.speedHistory = p.speedHistory[1:]
}
p.speedHistoryMu.Unlock()
}

// LastSpeed implements C.Proxy - returns fresh speed or 0 if stale
func (p *Proxy) LastSpeed() uint64 {
at := time.Unix(0, p.currentAt.Load())
if at.IsZero() || time.Since(at) > speedStaleThreshold {
return 0
}
return p.currentSpeed.Load()
}

// EffectiveSpeed implements C.Proxy - max(decayed peak, fresh current)
func (p *Proxy) EffectiveSpeed() uint64 {
current := p.LastSpeed()
peak := uint64(float64(p.peakSpeed.Load()) * math.Exp(-speedDecayLambda*time.Since(time.Unix(0, p.peakAt.Load())).Seconds()))
if peak > current {
return peak
}
return current
}

// SpeedHistory implements C.Proxy
func (p *Proxy) SpeedHistory() []C.SpeedHistory {
p.speedHistoryMu.Lock()
out := make([]C.SpeedHistory, len(p.speedHistory))
copy(out, p.speedHistory)
p.speedHistoryMu.Unlock()
return out
}

// PushTestResult implements C.Proxy - record test result for packet loss calculation
func (p *Proxy) PushTestResult(url string, success bool) {
p.testResultsMu.Lock()
p.testResults = append(p.testResults, TestResult{At: time.Now(), Success: success})
if len(p.testResults) > defaultTestResultsNum {
p.testResults = p.testResults[1:]
}
p.testResultsMu.Unlock()
}

// PacketLossRate implements C.Proxy - calculate loss rate from recent test results
func (p *Proxy) PacketLossRate(url string) float64 {
p.testResultsMu.Lock()
defer p.testResultsMu.Unlock()
if len(p.testResults) == 0 {
return 0
}
fails := 0
for _, r := range p.testResults {
if !r.Success {
fails++
}
}
return float64(fails) / float64(len(p.testResults))
}

func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions adapter/outboundgroup/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return proxy
}

func (f *Fallback) EffectiveSpeed() uint64 {
return f.findAliveProxy(false).EffectiveSpeed()
}

func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch)
for _, proxy := range proxies {
Expand Down
17 changes: 9 additions & 8 deletions adapter/outboundgroup/groupbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,19 +233,20 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus uti
var lock sync.Mutex
mp := map[string]uint16{}
proxies := gb.GetProxies(false)
for _, proxy := range proxies {
proxy := proxy
for _, p := range proxies {
if p == nil {
continue
}
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, url, expectedStatus)
go func(p C.Proxy) {
defer wg.Done()
delay, err := p.URLTest(ctx, url, expectedStatus)
if err == nil {
lock.Lock()
mp[proxy.Name()] = delay
mp[p.Name()] = delay
lock.Unlock()
}

wg.Done()
}()
}(p)
}
wg.Wait()

Expand Down
98 changes: 98 additions & 0 deletions adapter/outboundgroup/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package outboundgroup
import (
"errors"
"fmt"
"math"
"strings"

"github.com/dlclark/regexp2"
Expand Down Expand Up @@ -184,6 +185,12 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
case "load-balance":
strategy := parseStrategy(config)
return NewLoadBalance(groupOption, providers, strategy)
case "smart":
opts, err := parseSmartOption(config)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
return NewSmart(groupOption, providers, opts...)
case "relay":
return nil, fmt.Errorf("%w: The group [%s] with relay type was removed, please using dialer-proxy instead", errType, groupName)
default:
Expand Down Expand Up @@ -230,3 +237,94 @@ func addTestUrlToProviders(providers []P.ProxyProvider, url string, expectedStat
pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval)
}
}

func parseSmartOption(config map[string]any) ([]smartOption, error) {
opts := []smartOption{}

weightDelay := defaultWeightDelay
weightLoss := defaultWeightLoss
weightSpeed := defaultWeightSpeed
tolerance := defaultTolerance

if v, ok := config["weight-delay"]; ok {
if f, ok := parseNumeric(v); ok {
if f < 0 || math.IsNaN(f) || math.IsInf(f, 0) {
return nil, fmt.Errorf("weight-delay must be >= 0")
}
weightDelay = f
}
}
if v, ok := config["weight-loss"]; ok {
if f, ok := parseNumeric(v); ok {
if f < 0 || math.IsNaN(f) || math.IsInf(f, 0) {
return nil, fmt.Errorf("weight-loss must be >= 0")
}
weightLoss = f
}
}
if v, ok := config["weight-speed"]; ok {
if f, ok := parseNumeric(v); ok {
if f < 0 || math.IsNaN(f) || math.IsInf(f, 0) {
return nil, fmt.Errorf("weight-speed must be >= 0")
}
weightSpeed = f
}
}

sum := weightDelay + weightLoss + weightSpeed
if sum == 0 {
return nil, fmt.Errorf("at least one weight must be > 0")
}
weightDelay /= sum
weightLoss /= sum
weightSpeed /= sum

if v, ok := config["tolerance"]; ok {
if f, ok := parseNumeric(v); ok {
if f < 0 {
return nil, fmt.Errorf("tolerance must be >= 0")
}
tolerance = f
}
}

opts = append(opts, func(s *Smart) {
s.weightDelay = weightDelay
s.weightLoss = weightLoss
s.weightSpeed = weightSpeed
s.tolerance = tolerance
})

return opts, nil
}

func parseNumeric(v any) (float64, bool) {
switch x := v.(type) {
case float64:
return x, true
case float32:
return float64(x), true
case int:
return float64(x), true
case int8:
return float64(x), true
case int16:
return float64(x), true
case int32:
return float64(x), true
case int64:
return float64(x), true
case uint:
return float64(x), true
case uint8:
return float64(x), true
case uint16:
return float64(x), true
case uint32:
return float64(x), true
case uint64:
return float64(x), true
default:
return 0, false
}
}
4 changes: 4 additions & 0 deletions adapter/outboundgroup/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return s.selectedProxy(touch)
}

func (s *Selector) EffectiveSpeed() uint64 {
return s.selectedProxy(false).EffectiveSpeed()
}

func (s *Selector) selectedProxy(touch bool) C.Proxy {
proxies := s.GetProxies(touch)
for _, proxy := range proxies {
Expand Down
Loading