Skip to content
Merged
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
42 changes: 39 additions & 3 deletions docs/ACCELERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,45 @@ denominator: QUIC can send already-queued bytes during an application wait.
Actual transport backpressure remains outside token sleep. When a slower path
fills the finite send buffers, predominantly transport-bound samples can age
out the old bandwidth maximum. Sleep share is still an application-level
heuristic, not proof that the path is uncongested. The controller does not add
a separate capacity-probing phase: sustained RTT penalties can limit discovery
of spare capacity until conditions or higher delivery observations change.
heuristic, not proof that the path is uncongested.

For `balanced` and `aggressive`, a bounded capacity-recovery probe can break a
low-rate lock-in after real backpressure subsides while RTT stays elevated.
It requires at least three consecutive valid, loss-free, pacing-limited samples
spanning at least one second on the controller clock, without a material RTT
increase. It runs only when the steady target is below 95% of learned capacity.
The temporary admission rate is at most 1.25 times capacity (`balanced`) or
1.50 times capacity (`aggressive`), also capped at twice the steady target and
the configured maximum. Only a real higher wire sample can raise the capacity
estimate; starting or finishing a probe does not add tokens or change history.
The reported `TargetBytesPerSecond` remains the steady target, not this transient
admission rate.

The connection-wide byte allowance covers two bursts or two sampling windows,
whichever is larger, with a hard 1 MiB ceiling. The deadline covers service of
that allowance plus one sample window, or two smoothed RTTs, with a 100 ms
minimum and a two-second maximum. Configurations whose complete allowance
cannot fit those byte/time bounds are not probed. Deadline accounting splits
token refill at expiry even without another observation; cancellation refunds
normal tokens but never replenishes the probe allowance. Admission can finish
before the last wire feedback, so a bounded 250–500 ms grace period accepts
that outcome without allowing further probe-rate admission.

Any newly observed loss, a relative RTT rise above 25% (with a 1 ms noise
tolerance), or transport-bound delivery without a real capacity increase ends
the probe. Idle intervals and counter resets clear qualification. Successful
probes require more than 2% observed capacity growth and are separated by at
least one second or eight smoothed RTTs (the latter capped at 16 seconds).
Unsuccessful attempts back off from two to at most 16 seconds. RTT/loss penalties
continue to determine the steady target; the underlying QUIC controller remains
active. `conservative`, fixed-rate, and bypass modes do not perform these probes.

This is bounded recovery, not a guarantee of recovering all spare capacity:
large bandwidth-delay products or custom bursts can exceed the probe limits;
long-delayed feedback and persistent congestion can prevent discovery. Tests
cover fixed-RTT synthetic fast/slow/fast phases and a small-window real loopback
QUIC receiver slowdown. They do not establish Internet throughput, fairness,
browser similarity, or performance superiority.

This is intentionally not a full BBR state machine. In particular AutoCAR has
no transport-visible BDP congestion window, ACK aggregation model, ProbeRTT
Expand Down
170 changes: 170 additions & 0 deletions internal/accel/capacity_probe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package accel

import (
"math"
"time"
)

const (
maximumProbeBytes = 1 << 20
maximumProbeDuration = 2 * time.Second
minimumProbeGap = time.Second
maximumProbeBackoff = 16 * time.Second
)

// capacityProbe is a temporary, connection-wide admission excursion. It never
// writes into the estimator: only ordinary transport observations can establish
// higher capacity. All fields are protected by Controller.mu.
type capacityProbe struct {
active bool
rate int64
remaining float64
deadline time.Time
entryCapacity float64
entryRTT time.Duration
window time.Duration
gap time.Duration

// The last admitted bytes may not yet appear in wire counters. Allow a
// bounded feedback grace period after admission ends, without more probes.
pending bool
outcomeDeadline time.Time
backoff time.Duration
nextAt time.Time
eligibleSamples int
eligibleSince time.Time
eligibleRTT time.Duration
}

func (c *Controller) pacingRateLocked() int64 {
if c.probe.active {
return c.probe.rate
}
return c.targetRate
}

func (c *Controller) probeRTTRoseLocked(rtt time.Duration) bool {
baseline := c.probe.entryRTT
if !c.probe.active && !c.probe.pending && c.probe.eligibleSamples > 0 {
baseline = c.probe.eligibleRTT
} else if !c.probe.active && !c.probe.pending {
return false
}
// An absolute 1ms tolerance avoids treating microsecond scheduler noise on
// loopback as a newly growing queue. Sustained RTT penalties still apply to
// the steady target, independently of this relative abort signal.
return rtt > baseline && rtt-baseline > max(time.Millisecond, baseline/4)
}

func (c *Controller) resetProbeLocked() {
c.refillLocked(c.clock.Now())
wasActive := c.probe.active
c.probe = capacityProbe{}
if wasActive {
c.signalStateChangeLocked()
}
}

func (c *Controller) abortProbeLocked(now time.Time) {
if c.probe.active || c.probe.pending {
c.resolveProbeLocked(now, false)
}
c.probe.eligibleSamples = 0
}

func (c *Controller) resolveProbeLocked(now time.Time, success bool) {
wasActive := c.probe.active
c.probe.active = false
c.probe.pending = false
c.probe.eligibleSamples = 0
if success {
c.probe.backoff = 0
} else {
c.probe.backoff = min(maximumProbeBackoff, max(2*minimumProbeGap, 2*c.probe.backoff))
}
c.probe.nextAt = now.Add(max(c.probe.gap, c.probe.backoff))
if wasActive {
c.signalStateChangeLocked()
}
}

// Caller has already refilled through now at the old rate. The grace period is
// only for learning the result; no more high-rate admission is allowed.
func (c *Controller) finishProbeLocked(now time.Time) {
c.probe.active = false
c.probe.pending = true
c.probe.outcomeDeadline = now.Add(max(250*time.Millisecond, 2*c.probe.window))
c.probe.eligibleSamples = 0
c.signalStateChangeLocked()
}

func (c *Controller) observeProbeLocked(snapshot Snapshot, now time.Time, limited, loss bool) {
if c.profile == ProfileConservative {
return
}
capacity := c.estimator.bandwidthEstimate()
if c.probe.active || c.probe.pending {
// The normal estimator has consumed this sample first, including real
// backpressure and any higher delivery. Never discard evidence on abort.
if loss || c.probeRTTRoseLocked(snapshot.SmoothedRTT) {
c.abortProbeLocked(now)
} else if capacity > c.probe.entryCapacity*1.02 {
c.resolveProbeLocked(now, true)
} else if !limited {
c.abortProbeLocked(now)
}
return
}
if loss || !limited || c.estimator.count == 0 || float64(c.targetRate) >= capacity*.95 {
c.probe.eligibleSamples = 0
return
}
if c.probeRTTRoseLocked(snapshot.SmoothedRTT) {
c.probe.eligibleSamples = 0
}
if c.probe.eligibleSamples == 0 {
c.probe.eligibleSince = now
c.probe.eligibleRTT = snapshot.SmoothedRTT
}
if c.probe.eligibleSamples < 3 {
c.probe.eligibleSamples++
}
if c.probe.eligibleSamples < 3 || now.Sub(c.probe.eligibleSince) < time.Second || now.Before(c.probe.nextAt) {
return
}

gain := 1.25
if c.profile == ProfileAggressive {
gain = 1.50
}
rate := int64(math.Min(float64(c.maximumRate), math.Min(capacity*gain, 2*float64(c.targetRate))))
if float64(rate) <= capacity || rate <= c.targetRate {
return
}
window := stableSampleWindow(snapshot.MinRTT)
budget := math.Ceil(math.Max(2*c.burst, 2*float64(rate)*window.Seconds()))
// Do not repeatedly run probes too small/short to produce even a complete
// sample. Extremely large custom bursts or bandwidth-delay products are
// outside this deliberately bounded application-level recovery mechanism.
if budget > maximumProbeBytes {
return
}
service := durationForBytes(budget, rate)
if service > maximumProbeDuration-window {
return
}
duration := max(100*time.Millisecond, service+window)
// Clamp before multiplying potentially untrusted positive RTT durations.
duration = max(duration, 2*min(snapshot.SmoothedRTT, maximumProbeDuration/2))
gap := max(minimumProbeGap, 8*min(snapshot.SmoothedRTT, maximumProbeBackoff/8))
c.probe.active = true
c.probe.rate = rate
c.probe.remaining = budget
c.probe.deadline = now.Add(duration)
c.probe.entryCapacity = capacity
c.probe.entryRTT = snapshot.SmoothedRTT
c.probe.window = window
c.probe.gap = gap
c.probe.eligibleSamples = 0
c.signalStateChangeLocked()
}
Loading
Loading