Skip to main content

ConcurrencyRamp

Struct ConcurrencyRamp 

Source
pub struct ConcurrencyRamp { /* private fields */ }
Expand description

Drives concurrency upward on a live transfer while it pays to do so.

Implementations§

Source§

impl ConcurrencyRamp

Source

pub fn starting_at(min_gain_frac: f64, start: usize, max: usize) -> Self

min_gain_frac is the marginal goodput, as a fraction of the single-connection rate, that a new connection must add to be kept. Start the search at start connections rather than at one.

§Why the search no longer climbs from one

Climbing costs a measurement window per level, and a window long enough to outlast slow start (see WINDOW_DELTAS) is long enough that the climb dominates a short transfer. Climbing from one is only worth it if the levels above one are likely to be much better — and on the paths measured, they are not.

The asymmetry, not a claimed win, is what justifies starting low. Measured over 20 paired repetitions on four objects, starting at 1 connection is statistically indistinguishable from a fixed baseline while fixed -x 8 cost 1.37–3.04x, and on a path a single stream already saturates -x 8 incurred a 3.6x slowdown where -x 1 was 1.17x. So the downside of starting high is large and measured; the upside is not.

Starting at one is therefore a conservative policy choice, ensuring minimal overhead while admitting more connections only when headroom is proven.

Source

pub fn new(min_gain_frac: f64, max: usize) -> Self

Source

pub fn arm_warmup(&mut self, now: f64, delta: f64)

Opt in to the warm-up gate, and arm it for the level the search starts at.

§The measurement this exists to prevent

The windows are scaled by delta, the per-REQUEST setup cost, which on a pooled connection is one round trip and is measured at 50-100 ms on the paths this was tuned against. Admitting a CONNECTION costs something else entirely: a TCP handshake, a TLS handshake and a first byte, and on a 250 ms-RTT path that is 1.2-1.6 s before a single byte arrives — longer than SETTLE_DELTAS and WINDOW_DELTAS together, both of which are clamped at MAX_WINDOW_S.

The window therefore opened and closed while the new connection was still handshaking, the level measured as no better than the one below it, and the search settled at ONE on a path with real headroom. Reported from the field as “only two of eight connections start”: the second connection had delivered 240 KB — the tail of its slow start — when the ramp judged it and stopped.

A low-RTT path escapes it by luck: there the handshake fits inside the settle delay, so the same code measures a warm connection and climbs to the ceiling. That is what made this look path-specific rather than systematic.

So the settle delay cannot be a duration alone. The transport reports how many connections are actually delivering (note_delivering), and the window does not open until the level’s connections are among them, or until the deadline this arms expires — a connection that never delivers must not stall the search forever; the scheduler’s own stall detectors own that case.

Source

pub fn note_delivering(&mut self, n: usize)

Report how many connections are currently delivering bytes.

Aggregate count, not a set: the gate only asks whether the level it is about to measure is fully on the wire.

Source

pub fn start(&mut self, now: f64, delta: f64)

Begin the first window. now is the transfer’s clock, delta the measured per-request setup cost.

Source

pub fn observe(&mut self, bytes: u64, now: f64)

Record bytes delivered by the whole transfer.

Aggregate, not per-connection: the question is whether the path is carrying more, and a per-connection view cannot answer it — on a saturated link each connection’s own rate falls as connections are added while the total stays flat, which is exactly the case the ramp must detect.

Source

pub fn settled(&self) -> Option<usize>

The useful count, once the search has settled.

Source

pub fn level(&self) -> usize

Current concurrency.

Source

pub fn clamp_max(&mut self, max: usize)

Lower the ceiling the search may reach, and the level it is holding with it.

Called when the origin refuses a request and the transfer learns a limit smaller than the connection budget. Two things go wrong without it. The search keeps doubling toward a count it can never be given, so every level above the limit measures the same rate and the search is deciding on readings that describe the same concurrency. And the warm-up gate waits for level connections to deliver — connections the refusal has clamped away — so every level burns its whole WARM_DELTAS deadline before it is judged.

Only ever downward: a ceiling learned from a refusal must not be raised by the search that provoked it. The transfer loop owns probing back up.

Source

pub fn poll(&mut self, now: f64, delta: f64) -> Ramp

Close the window if it is due and decide what to do next.

Trait Implementations§

Source§

impl Clone for ConcurrencyRamp

Source§

fn clone(&self) -> ConcurrencyRamp

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ConcurrencyRamp

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.