Files
fscan/core/scan_metrics_test.go
T
ZacharyZcR 88c7e4f2be feat: 自适应并发调度 — 网络探测 + AIMD + 参数智能推导
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据
推导 6 个关键参数,替代硬编码默认值:

- Timeout: median_RTT + 4σ(覆盖 99.9% 正常连接)
- ModuleThreadNum: target_concurrency / 30
- MaxRetries: ceil(log(0.01)/log(loss_rate))(全失败概率 <1%)
- ICMPRate: 环境基准 × fd 系数
- PocNum: 跟随 ModuleThreadNum
- DisablePing: 已有 ICMP 权限降级机制

线程池从单信号(资源耗尽率)升级为 AIMD + 慢启动:
- 慢启动:target/4 起步,500ms 翻倍
- 稳态 AIMD:健康 +5%,拥塞 ×0.5
- 双信号:资源耗尽率 + RTT 趋势(双 EMA)

用户 -t 显式指定时作为 ceiling,探测仍调整其他参数。

测试:单元 + 边界 + 集成 + 真实网络,core 包 580+ 用例全通过。
2026-06-14 22:23:45 +08:00

131 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"sync"
"testing"
"time"
)
// =============================================================================
// 单元测试:ScanMetrics 基本操作
// =============================================================================
func TestScanMetrics_Counters(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(time.Millisecond)
m.RecordConnect(2 * time.Millisecond)
m.RecordRefused(500 * time.Microsecond)
m.RecordTimeout()
m.RecordExhausted()
if m.Total() != 5 {
t.Errorf("Total() = %d, want 5", m.Total())
}
snap := m.Snapshot()
if snap.Connects != 2 {
t.Errorf("Connects = %d, want 2", snap.Connects)
}
if snap.Refused != 1 {
t.Errorf("Refused = %d, want 1", snap.Refused)
}
if snap.Timeouts != 1 {
t.Errorf("Timeouts = %d, want 1", snap.Timeouts)
}
if snap.Exhausted != 1 {
t.Errorf("Exhausted = %d, want 1", snap.Exhausted)
}
}
// =============================================================================
// 单元测试:RTT EMA 收敛
// =============================================================================
func TestScanMetrics_RTT_EMA(t *testing.T) {
m := &ScanMetrics{}
// 喂入稳定的 10ms RTT
for i := 0; i < 100; i++ {
m.RecordConnect(10 * time.Millisecond)
}
fast := m.RTTFast()
if fast < 9*time.Millisecond || fast > 11*time.Millisecond {
t.Errorf("稳定 10ms 后 RTTFast = %v, 应该接近 10ms", fast)
}
ratio := m.RTTRatio()
if ratio < 0.9 || ratio > 1.1 {
t.Errorf("稳定状态 RTTRatio = %.2f, 应该接近 1.0", ratio)
}
}
func TestScanMetrics_RTT_Trend(t *testing.T) {
m := &ScanMetrics{}
// 先喂入 100 个 5ms 建立基线
for i := 0; i < 100; i++ {
m.RecordConnect(5 * time.Millisecond)
}
// 再喂入 50 个 50msRTT 突增 10 倍)
for i := 0; i < 50; i++ {
m.RecordConnect(50 * time.Millisecond)
}
ratio := m.RTTRatio()
// fast EMA 应该比 slow EMA 高(fast 跟踪快,slow 还没追上来)
if ratio <= 1.0 {
t.Errorf("RTT 突增后 RTTRatio = %.2f, 应该 > 1.0", ratio)
}
t.Logf("RTT 突增后: ratio=%.2f, fast=%v", ratio, m.RTTFast())
}
func TestScanMetrics_RTT_InsufficientSamples(t *testing.T) {
m := &ScanMetrics{}
// 少于 20 个样本
for i := 0; i < 10; i++ {
m.RecordConnect(time.Millisecond)
}
ratio := m.RTTRatio()
if ratio != 1.0 {
t.Errorf("样本不足时 RTTRatio = %.2f, 应该是 1.0", ratio)
}
}
// =============================================================================
// 并发安全测试
// =============================================================================
func TestScanMetrics_ConcurrentSafety(t *testing.T) {
m := &ScanMetrics{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(4)
go func() { defer wg.Done(); m.RecordConnect(time.Millisecond) }()
go func() { defer wg.Done(); m.RecordRefused(time.Millisecond) }()
go func() { defer wg.Done(); m.RecordTimeout() }()
go func() { defer wg.Done(); m.RecordExhausted() }()
}
wg.Wait()
if m.Total() != 400 {
t.Errorf("并发后 Total() = %d, want 400", m.Total())
}
// 验证 Snapshot 不 panic
snap := m.Snapshot()
if snap.Total() != 400 {
t.Errorf("并发后 Snapshot.Total() = %d, want 400", snap.Total())
}
// 验证 RTTRatio 不 panic
_ = m.RTTRatio()
}