Mult Port + TCP Calibration (SSH DEAD)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
@@ -22,6 +23,8 @@ type chunkClientOptions struct {
|
||||
maxSize int
|
||||
adaptive bool
|
||||
adaptSuccesses int
|
||||
shrinkAfter int
|
||||
shrinkStep int
|
||||
adaptLog bool
|
||||
pollers int
|
||||
reconnectEvery int
|
||||
@@ -33,6 +36,7 @@ type chunkClientOptions struct {
|
||||
headerMask byte
|
||||
coverProfile cover.Profile
|
||||
skipPathProbe bool
|
||||
forceMaxStart bool
|
||||
}
|
||||
|
||||
type adaptiveSizer struct {
|
||||
@@ -43,6 +47,9 @@ type adaptiveSizer struct {
|
||||
max int
|
||||
adaptive bool
|
||||
adaptSuccesses int
|
||||
shrinkAfter int
|
||||
shrinkStep int
|
||||
failures int
|
||||
successes int
|
||||
good int
|
||||
bad int
|
||||
@@ -68,6 +75,13 @@ func newAdaptiveSizer(name string, start int, opts chunkClientOptions) *adaptive
|
||||
}
|
||||
return 64
|
||||
}(),
|
||||
shrinkAfter: func() int {
|
||||
if opts.shrinkAfter > 0 {
|
||||
return opts.shrinkAfter
|
||||
}
|
||||
return 1
|
||||
}(),
|
||||
shrinkStep: opts.shrinkStep,
|
||||
logChanges: opts.adaptLog,
|
||||
}
|
||||
}
|
||||
@@ -81,7 +95,14 @@ func (s *adaptiveSizer) Current() int {
|
||||
func (s *adaptiveSizer) Success(attempted int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.adaptive || attempted != s.current || s.current >= s.max {
|
||||
if !s.adaptive || attempted != s.current {
|
||||
return
|
||||
}
|
||||
// A real success proves the current size still works. In max-first mode this
|
||||
// resets the consecutive-failure budget so isolated carrier errors never
|
||||
// cause a downshift.
|
||||
s.failures = 0
|
||||
if s.current >= s.max {
|
||||
return
|
||||
}
|
||||
if attempted > s.good {
|
||||
@@ -135,11 +156,25 @@ func (s *adaptiveSizer) FailureReason(attempted int, cause error) (int, int) {
|
||||
return old, old
|
||||
}
|
||||
s.successes = 0
|
||||
s.failures++
|
||||
if s.failures < s.shrinkAfter {
|
||||
if s.logChanges && s.shrinkAfter > 1 {
|
||||
fmt.Printf("adaptive %s chunk: holding %d after failure %d/%d: %v\n", s.name, old, s.failures, s.shrinkAfter, cause)
|
||||
}
|
||||
return old, old
|
||||
}
|
||||
s.failures = 0
|
||||
if s.bad == 0 || attempted < s.bad {
|
||||
s.bad = attempted
|
||||
}
|
||||
next := attempted / 2
|
||||
if s.good > 0 && s.good < attempted {
|
||||
if s.shrinkStep > 0 {
|
||||
next = attempted - s.shrinkStep
|
||||
// MAX-FIRST linear downgrade intentionally ignores an older known-good
|
||||
// point: the goal is to walk down in small deterministic steps instead
|
||||
// of making a large jump after a transient carrier rejection.
|
||||
s.good = 0
|
||||
} else if s.good > 0 && s.good < attempted {
|
||||
next = s.good
|
||||
} else {
|
||||
s.good = 0
|
||||
@@ -470,6 +505,123 @@ func probeOne(serverAddr, token string, opts chunkClientOptions, kind byte, cand
|
||||
return status == wire.StatusOK
|
||||
}
|
||||
|
||||
type iperfProbeResult struct {
|
||||
ok bool
|
||||
bytes int
|
||||
elapsed time.Duration
|
||||
err error
|
||||
}
|
||||
|
||||
func (r iperfProbeResult) mbps() float64 {
|
||||
if r.elapsed <= 0 || r.bytes <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (float64(r.bytes) * 8) / r.elapsed.Seconds() / 1_000_000
|
||||
}
|
||||
|
||||
func calibrationDeadline(opts chunkClientOptions) time.Duration {
|
||||
t := opts.txnTimeout
|
||||
if t < 8*time.Second {
|
||||
t = 8 * time.Second
|
||||
}
|
||||
if t > 20*time.Second {
|
||||
t = 20 * time.Second
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func dialProbeConn(serverAddr string, opts chunkClientOptions) (net.Conn, error) {
|
||||
d := net.Dialer{Timeout: minDuration(calibrationDeadline(opts), 10*time.Second), KeepAlive: 30 * time.Second}
|
||||
conn, err := d.Dial("tcp", serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cover.WritePreface(conn, opts.coverProfile); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
protocol.TuneTCP(conn)
|
||||
protocol.TuneTCPBuffer(conn, opts.tcpBuffer)
|
||||
_ = conn.SetDeadline(time.Now().Add(calibrationDeadline(opts)))
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// probeIperfOne validates one UP or DW record at a time against the DragonTCP
|
||||
// server. Calibration is intentionally single-poller and never pipelines
|
||||
// multiple outstanding records: it measures a single lane's safe record size,
|
||||
// not aggregate throughput. Boundary confirmation retries happen sequentially
|
||||
// on fresh connections.
|
||||
func probeIperfOne(serverAddr, token string, opts chunkClientOptions, kind byte, candidate int) iperfProbeResult {
|
||||
count := wire.ProbeBurstCount(candidate)
|
||||
totalBytes := candidate * count
|
||||
conn, err := dialProbeConn(serverAddr, opts)
|
||||
if err != nil {
|
||||
return iperfProbeResult{err: err}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
return iperfProbeResult{err: err}
|
||||
}
|
||||
startSeq := probeSeq.Add(uint64(count)) - uint64(count) + 1
|
||||
started := time.Now()
|
||||
|
||||
switch kind {
|
||||
case wire.ProbeUpload:
|
||||
payload := makeProbePayload(wire.ProbeIperfUpload, candidate, candidate, token)
|
||||
totalBytes = len(payload) * count
|
||||
for i := 0; i < count; i++ {
|
||||
seq := startSeq + uint64(i)
|
||||
if err := wire.WriteRequestProfileEncoding(conn, wire.ModeProbe, sid, seq, payload, opts.headerMask, opts.coverProfile.Clear); err != nil {
|
||||
return iperfProbeResult{bytes: i * len(payload), elapsed: time.Since(started), err: err}
|
||||
}
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
status, body, err := wire.ReadResponseProfile(conn, opts.headerMask)
|
||||
if err != nil {
|
||||
return iperfProbeResult{bytes: totalBytes, elapsed: time.Since(started), err: err}
|
||||
}
|
||||
if status != wire.StatusOK {
|
||||
return iperfProbeResult{bytes: totalBytes, elapsed: time.Since(started), err: fmt.Errorf("upload status=%d body=%s", status, string(body))}
|
||||
}
|
||||
}
|
||||
return iperfProbeResult{ok: true, bytes: totalBytes, elapsed: time.Since(started)}
|
||||
|
||||
case wire.ProbeDownload:
|
||||
payload := makeProbePayload(wire.ProbeIperfDownload, candidate, 0, token)
|
||||
if err := wire.WriteRequestProfileEncoding(conn, wire.ModeProbe, sid, startSeq, payload, opts.headerMask, opts.coverProfile.Clear); err != nil {
|
||||
return iperfProbeResult{elapsed: time.Since(started), err: err}
|
||||
}
|
||||
want := probePattern(candidate)
|
||||
gotBytes := 0
|
||||
for i := 0; i < count; i++ {
|
||||
status, body, err := wire.ReadResponseProfile(conn, opts.headerMask)
|
||||
if err != nil {
|
||||
return iperfProbeResult{bytes: gotBytes, elapsed: time.Since(started), err: err}
|
||||
}
|
||||
seq := startSeq + uint64(i)
|
||||
if status != wire.StatusError && len(body) > 0 && !opts.coverProfile.Clear {
|
||||
body = wire.DecodeMaskedResponse(status, body, sid, wire.ModeProbe, seq)
|
||||
}
|
||||
if status != wire.StatusData || !bytes.Equal(body, want) {
|
||||
return iperfProbeResult{bytes: gotBytes, elapsed: time.Since(started), err: fmt.Errorf("download validation failed status=%d len=%d want=%d", status, len(body), candidate)}
|
||||
}
|
||||
gotBytes += len(body)
|
||||
}
|
||||
return iperfProbeResult{ok: true, bytes: gotBytes, elapsed: time.Since(started)}
|
||||
default:
|
||||
return iperfProbeResult{err: fmt.Errorf("unknown iperf probe kind %d", kind)}
|
||||
}
|
||||
}
|
||||
|
||||
func minDuration(a, b time.Duration) time.Duration {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func probePersistent(serverAddr, token string, opts chunkClientOptions) bool {
|
||||
sid, err := randomSessionID()
|
||||
if err != nil {
|
||||
@@ -512,28 +664,268 @@ func probeCandidates(minSize, maxSize int) []int {
|
||||
return out
|
||||
}
|
||||
|
||||
func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte) int {
|
||||
candidates := probeCandidates(opts.minSize, opts.maxSize)
|
||||
lo, hi := 0, len(candidates)-1
|
||||
best := opts.minSize
|
||||
for lo <= hi {
|
||||
mid := lo + (hi-lo)/2
|
||||
candidate := candidates[mid]
|
||||
if probeOne(serverAddr, token, opts, kind, candidate) {
|
||||
best = candidate
|
||||
lo = mid + 1
|
||||
} else {
|
||||
hi = mid - 1
|
||||
const calibrationFineResolution = 32
|
||||
|
||||
const calibrationDecisionAttempts = 3
|
||||
|
||||
// confirmIperfResult makes boundary decisions resistant to one transient
|
||||
// carrier hiccup. The caller provides the first observation; this function
|
||||
// opens fresh probe connections until either success or non-timeout failure has
|
||||
// a 2-of-3 majority. Connection-level timeouts are inconclusive and never count
|
||||
// as proof that a chunk size is too large.
|
||||
func confirmIperfResult(serverAddr, token string, opts chunkClientOptions, kind byte, candidate int, first iperfProbeResult, stage string) iperfProbeResult {
|
||||
name := probeKindName(kind)
|
||||
successes, failures := 0, 0
|
||||
var lastSuccess, lastFailure, lastTimeout iperfProbeResult
|
||||
|
||||
observe := func(r iperfProbeResult) {
|
||||
if r.ok {
|
||||
successes++
|
||||
lastSuccess = r
|
||||
return
|
||||
}
|
||||
if isTransportTimeout(r.err) {
|
||||
lastTimeout = r
|
||||
return
|
||||
}
|
||||
failures++
|
||||
lastFailure = r
|
||||
}
|
||||
if best < opts.minSize {
|
||||
best = opts.minSize
|
||||
|
||||
observe(first)
|
||||
for attempt := 2; attempt <= calibrationDecisionAttempts && successes < 2 && failures < 2; attempt++ {
|
||||
r := probeIperfOne(serverAddr, token, opts, kind, candidate)
|
||||
observe(r)
|
||||
result := "failure"
|
||||
if r.ok {
|
||||
result = "success"
|
||||
} else if isTransportTimeout(r.err) {
|
||||
result = "connection_timeout"
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=%s chunk=%d confirmation=%d/%d result=%s\n", name, stage, candidate, attempt, calibrationDecisionAttempts, result)
|
||||
}
|
||||
return best
|
||||
|
||||
if successes >= 2 {
|
||||
return lastSuccess
|
||||
}
|
||||
if failures >= 2 {
|
||||
return lastFailure
|
||||
}
|
||||
// No majority means connection health was too unstable to classify the
|
||||
// candidate. Preserve the old timeout semantics by reporting the timeout and
|
||||
// keeping the last known-good size.
|
||||
if lastTimeout.err != nil {
|
||||
return lastTimeout
|
||||
}
|
||||
if successes > failures && lastSuccess.ok {
|
||||
return lastSuccess
|
||||
}
|
||||
return lastFailure
|
||||
}
|
||||
|
||||
func probeMaximum(serverAddr, token string, opts chunkClientOptions, kind byte) int {
|
||||
name := probeKindName(kind)
|
||||
// Startup calibration precision is independent from the runtime shrink step.
|
||||
// Grow quickly, then resolve only the final good/bad boundary to 32 bytes.
|
||||
fine := calibrationFineResolution
|
||||
|
||||
// Ascending calibration deliberately starts small and grows geometrically in large steps.
|
||||
// This avoids hammering a constrained carrier with 1 MiB records before we
|
||||
// know they are viable, while still reaching the ceiling in O(log N) probes.
|
||||
// Once the first failure is found, binary refinement resolves the highest
|
||||
// known-good size to roughly `fine` bytes (32 B in the Android build).
|
||||
good := 0
|
||||
bad := 0
|
||||
candidate := opts.minSize
|
||||
if candidate < 32 {
|
||||
candidate = 32
|
||||
}
|
||||
if candidate > opts.maxSize {
|
||||
candidate = opts.maxSize
|
||||
}
|
||||
|
||||
for {
|
||||
result := probeIperfOne(serverAddr, token, opts, kind, candidate)
|
||||
if result.ok {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps())
|
||||
good = candidate
|
||||
if candidate >= opts.maxSize {
|
||||
return opts.maxSize
|
||||
}
|
||||
next := candidate * 4
|
||||
// Do not waste many tiny probes when the configured floor is very
|
||||
// small. After proving the floor, jump to at least 512 B and then
|
||||
// continue growing by 4x.
|
||||
if candidate == opts.minSize && next < 512 && opts.maxSize >= 512 {
|
||||
next = 512
|
||||
}
|
||||
if next > opts.maxSize {
|
||||
next = opts.maxSize
|
||||
}
|
||||
if next <= candidate {
|
||||
return good
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=ascend chunk_upgrade=%d->%d\n", name, candidate, next)
|
||||
candidate = next
|
||||
continue
|
||||
}
|
||||
|
||||
// A first failure may be transient. Re-test this exact size on fresh
|
||||
// connections before declaring the ascending ceiling.
|
||||
result = confirmIperfResult(serverAddr, token, opts, kind, candidate, result, "ascend-confirm")
|
||||
if result.ok {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d result=recovered_after_retry\n", name, candidate)
|
||||
good = candidate
|
||||
if candidate >= opts.maxSize {
|
||||
return opts.maxSize
|
||||
}
|
||||
next := candidate * 4
|
||||
if candidate == opts.minSize && next < 512 && opts.maxSize >= 512 {
|
||||
next = 512
|
||||
}
|
||||
if next > opts.maxSize {
|
||||
next = opts.maxSize
|
||||
}
|
||||
if next <= candidate {
|
||||
return good
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=ascend chunk_upgrade=%d->%d\n", name, candidate, next)
|
||||
candidate = next
|
||||
continue
|
||||
}
|
||||
|
||||
if isTransportTimeout(result.err) {
|
||||
// A real I/O timeout means the physical connection is dead. It is not
|
||||
// evidence that this record size is invalid, so stop calibration at
|
||||
// the last proven size instead of walking the size ladder.
|
||||
selected := good
|
||||
if selected == 0 {
|
||||
selected = opts.minSize
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d result=connection_timeout action=keep_known_good known_good=%d err=%v\n", name, candidate, selected, result.err)
|
||||
return selected
|
||||
}
|
||||
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=ascend chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps(), result.err)
|
||||
bad = candidate
|
||||
if good == 0 {
|
||||
return opts.minSize
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
for bad-good > fine {
|
||||
next := good + (bad-good)/2
|
||||
if next <= good || next >= bad {
|
||||
break
|
||||
}
|
||||
result := probeIperfOne(serverAddr, token, opts, kind, next)
|
||||
result = confirmIperfResult(serverAddr, token, opts, kind, next, result, "refine-confirm")
|
||||
if result.ok {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=refine chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps())
|
||||
good = next
|
||||
continue
|
||||
}
|
||||
if isTransportTimeout(result.err) {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=refine chunk=%d result=connection_timeout action=keep_known_good known_good=%d err=%v\n", name, next, good, result.err)
|
||||
return good
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=refine chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps(), result.err)
|
||||
bad = next
|
||||
}
|
||||
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=refine selected=%d failed_above=%d resolution=%d\n", name, good, bad, fine)
|
||||
return good
|
||||
}
|
||||
|
||||
func probeKindName(kind byte) string {
|
||||
if kind == wire.ProbeDownload {
|
||||
return "download"
|
||||
}
|
||||
return "upload"
|
||||
}
|
||||
|
||||
const (
|
||||
// A literal 200-byte descent from 1 MiB can require more than five thousand
|
||||
// carrier probes per direction. MAX-FIRST therefore finds a working region
|
||||
// with a bounded coarse descent, then resolves the final good/bad boundary to
|
||||
// approximately 32 bytes. This preserves fine sizing without probe floods.
|
||||
maxFirstCoarseStep = 128 * 1024
|
||||
maxFirstFineResolution = calibrationFineResolution
|
||||
)
|
||||
|
||||
// probeMaximumMaxFirst always validates the configured ceiling first. On a
|
||||
// recoverable size rejection it descends by 128 KiB until it finds a known-good
|
||||
// size, then binary-refines the interval between that good size and the nearest
|
||||
// failed size to <= 32 bytes. A connection-level timeout is never interpreted
|
||||
// as evidence that the chunk is too large, so it does not trigger a size sweep.
|
||||
func probeMaximumMaxFirst(serverAddr, token string, opts chunkClientOptions, kind byte) int {
|
||||
fine := maxFirstFineResolution
|
||||
candidate := opts.maxSize
|
||||
if candidate < opts.minSize {
|
||||
candidate = opts.minSize
|
||||
}
|
||||
name := probeKindName(kind)
|
||||
failedHigh := 0
|
||||
|
||||
// Coarse descent: at most about eight probes from 1 MiB to the bottom of
|
||||
// the normal range, instead of thousands of 200-byte requests.
|
||||
for {
|
||||
result := probeIperfOne(serverAddr, token, opts, kind, candidate)
|
||||
if result.ok {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=coarse chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps())
|
||||
if failedHigh == 0 {
|
||||
return candidate
|
||||
}
|
||||
break
|
||||
}
|
||||
if isTransportTimeout(result.err) {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=coarse chunk=%d result=connection_timeout action=keep_size_and_replace_connection err=%v\n", name, candidate, result.err)
|
||||
return candidate
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=coarse chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, candidate, wire.ProbeBurstCount(candidate), result.bytes, result.mbps(), result.err)
|
||||
failedHigh = candidate
|
||||
if candidate <= opts.minSize {
|
||||
return opts.minSize
|
||||
}
|
||||
next := candidate - maxFirstCoarseStep
|
||||
if next < opts.minSize {
|
||||
next = opts.minSize
|
||||
}
|
||||
if next >= candidate {
|
||||
next = opts.minSize
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=coarse chunk_downgrade=%d->%d step=%d\n", name, candidate, next, maxFirstCoarseStep)
|
||||
candidate = next
|
||||
}
|
||||
|
||||
// Fine boundary search. candidate is known-good and failedHigh is known-bad.
|
||||
good := candidate
|
||||
bad := failedHigh
|
||||
for bad-good > fine {
|
||||
next := good + (bad-good)/2
|
||||
if next <= good {
|
||||
break
|
||||
}
|
||||
result := probeIperfOne(serverAddr, token, opts, kind, next)
|
||||
if result.ok {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=fine chunk=%d records=%d bytes=%d mbps=%.2f result=success\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps())
|
||||
good = next
|
||||
continue
|
||||
}
|
||||
if isTransportTimeout(result.err) {
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=fine chunk=%d result=connection_timeout action=keep_known_good known_good=%d err=%v\n", name, next, good, result.err)
|
||||
return good
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION fake_iperf=%s stage=fine chunk=%d records=%d bytes=%d mbps=%.2f result=failure err=%v\n", name, next, wire.ProbeBurstCount(next), result.bytes, result.mbps(), result.err)
|
||||
bad = next
|
||||
}
|
||||
fmt.Printf("[D-TCP] phase=CALIBRATION probe=%s stage=fine selected=%d failed_above=%d resolution=%d\n", name, good, bad, fine)
|
||||
return good
|
||||
}
|
||||
|
||||
func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfile {
|
||||
key := fmt.Sprintf("%s|%s|%d|%d|%02x|%t|%04x|%d|%t", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear)
|
||||
key := fmt.Sprintf("%s|%s|%d|%d|%02x|%t|%04x|%d|%t|maxfirst=%t|shrink=%d|step=%d", serverAddr, token, opts.minSize, opts.maxSize, opts.headerMask, opts.coverProfile.Enabled, opts.coverProfile.ID, opts.coverProfile.Padding, opts.coverProfile.Clear, opts.forceMaxStart, opts.shrinkAfter, opts.shrinkStep)
|
||||
profileState.Lock()
|
||||
if profileState.key == key && time.Since(profileState.p.at) < 30*time.Minute {
|
||||
p := profileState.p
|
||||
@@ -545,23 +937,27 @@ func getPathProfile(serverAddr, token string, opts chunkClientOptions) pathProfi
|
||||
fallbackUp := minInt(opts.maxSize, maxInt(opts.minSize, 32768))
|
||||
fallbackDown := minInt(opts.maxSize, maxInt(opts.minSize, 1350))
|
||||
|
||||
upCh := make(chan int, 1)
|
||||
downCh := make(chan int, 1)
|
||||
go func() { upCh <- probeMaximum(serverAddr, token, opts, wire.ProbeUpload) }()
|
||||
go func() { downCh <- probeMaximum(serverAddr, token, opts, wire.ProbeDownload) }()
|
||||
|
||||
p := pathProfile{upload: fallbackUp, download: fallbackDown, persistent: false, at: time.Now()}
|
||||
select {
|
||||
case p.upload = <-upCh:
|
||||
case <-time.After(20 * time.Second):
|
||||
}
|
||||
select {
|
||||
case p.download = <-downCh:
|
||||
case <-time.After(20 * time.Second):
|
||||
if opts.forceMaxStart {
|
||||
// Legacy CLI-only mode retained for compatibility. The Android app no
|
||||
// longer exposes MAX FIRST and uses the ascending path below.
|
||||
p.upload = probeMaximumMaxFirst(serverAddr, token, opts, wire.ProbeUpload)
|
||||
p.download = probeMaximumMaxFirst(serverAddr, token, opts, wire.ProbeDownload)
|
||||
} else {
|
||||
// Keep UP and DW calibration sequential. Running both fake-iperf probes
|
||||
// together can look like a traffic burst and distort the carrier limit we
|
||||
// are trying to measure.
|
||||
p.upload = probeMaximum(serverAddr, token, opts, wire.ProbeUpload)
|
||||
p.download = probeMaximum(serverAddr, token, opts, wire.ProbeDownload)
|
||||
}
|
||||
p.persistent = probePersistent(serverAddr, token, opts)
|
||||
|
||||
fmt.Printf("path probe: upload=%d download=%d persistent=%t\n", p.upload, p.download, p.persistent)
|
||||
fmt.Printf("path probe: strategy=%s upload=%d download=%d persistent=%t\n", func() string {
|
||||
if opts.forceMaxStart {
|
||||
return "max-first"
|
||||
}
|
||||
return "ascending"
|
||||
}(), p.upload, p.download, p.persistent)
|
||||
|
||||
profileState.Lock()
|
||||
profileState.key = key
|
||||
@@ -649,6 +1045,12 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
if opts.adaptSuccesses < 1 {
|
||||
opts.adaptSuccesses = 64
|
||||
}
|
||||
if opts.shrinkAfter < 1 {
|
||||
opts.shrinkAfter = 1
|
||||
}
|
||||
if opts.forceMaxStart {
|
||||
opts.startSize = opts.maxSize
|
||||
}
|
||||
if opts.reconnectEvery < 0 {
|
||||
opts.reconnectEvery = 0
|
||||
}
|
||||
@@ -665,9 +1067,13 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
opts.minPipeline = opts.maxPipeline
|
||||
}
|
||||
|
||||
defaultStart := opts.minSize
|
||||
if opts.forceMaxStart {
|
||||
defaultStart = opts.maxSize
|
||||
}
|
||||
profile := pathProfile{
|
||||
upload: opts.minSize,
|
||||
download: opts.minSize,
|
||||
upload: defaultStart,
|
||||
download: defaultStart,
|
||||
persistent: false,
|
||||
at: time.Now(),
|
||||
}
|
||||
@@ -738,8 +1144,17 @@ func openChunkTunnel(serverAddr, token, targetHost string, targetPort int, opts
|
||||
minPipeline: opts.minPipeline,
|
||||
maxPipeline: opts.maxPipeline,
|
||||
}
|
||||
c.upSizer = newAdaptiveSizer("upload", upStart, opts)
|
||||
c.downSizer = newAdaptiveSizer("download", downStart, opts)
|
||||
upSizerOpts := opts
|
||||
downSizerOpts := opts
|
||||
if !opts.skipPathProbe {
|
||||
// Calibration is the path ceiling for this VPN session. Runtime
|
||||
// adaptation may shrink after repeated failures and recover to this
|
||||
// value, but it must not grow above a size the UP/DW test did not prove.
|
||||
upSizerOpts.maxSize = upStart
|
||||
downSizerOpts.maxSize = downStart
|
||||
}
|
||||
c.upSizer = newAdaptiveSizer("upload", upStart, upSizerOpts)
|
||||
c.downSizer = newAdaptiveSizer("download", downStart, downSizerOpts)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user