Skip to main content

ShapeRecorder

Struct ShapeRecorder 

Source
pub(crate) struct ShapeRecorder {
    classes: Vec<ClassCounters>,
    default_class: ClassCounters,
    unshapeable: ClassCounters,
    totals: [DirectionCounters; 2],
    proxy: Option<Arc<ProxyRecorder>>,
    proxy_sized: bool,
}
Expand description

Session-scoped shaping-counter storage.

One per ProxySession, held behind an Arc and cloned into every forwarding task exactly as Recorder is, so two sessions running side by side in one test binary cannot see each other’s increments.

Always constructed, including for a session with no profile, on the same reasoning StreamRegistry is. An unshaped session’s recorder has an empty class list and no writer, so it snapshots as ShapeStats::default(); making construction conditional would replace one always-zero allocation with an Option on the hot context and prove nothing extra.

Fields§

§classes: Vec<ClassCounters>§default_class: ClassCounters§unshapeable: ClassCounters§totals: [DirectionCounters; 2]

The session totals, one row per leg, indexed by Direction.

One recorder serves every forwarding task of a session, so a single row would answer “how much was shaped” and refuse to answer “on which side” — the question an author shaping both legs is actually asking. Two rows and an index cost the writers nothing: charging a total is still one relaxed fetch_add at a known offset.

§proxy: Option<Arc<ProxyRecorder>>

This session’s proxy’s counters, or None when it has no proxy.

Forwarded to by every writer below rather than reached by a separate call at each site. One call charges both recorders or neither, so there is no way to add a producer to a session figure and forget the proxy figure beside it — which is the failure a second set of call sites would make inevitable and silent.

None is not a degraded mode. A session constructed directly belongs to no proxy, so there is no aggregate for it to be part of, and it keeps reporting its own ShapeStats exactly as it always did.

§proxy_sized: bool

Whether this session’s class list is the one the proxy’s rows were sized from, which decides whether a Class::Rule(index) may address them. Resolved once at attach; see ProxyRecorder::row.

Implementations§

Source§

impl ShapeRecorder

Source

pub(crate) fn for_profile(profile: Option<&ShapeProfile>) -> Self

Pre-size the rows for profile’s classes, in configured order.

None gives a recorder with no class rows — the unshaped session’s shape, whose snapshot is ShapeStats::default().

The recorder this builds reports to no proxy. That is the right answer for a session driven directly, and Self::attached is what the accept loop uses instead.

Source

pub(crate) fn attached( profile: Option<&ShapeProfile>, proxy: Arc<ProxyRecorder>, ) -> Self

The same recorder, additionally reporting into proxy.

Built at the one moment a session is attached to a control plane — after it is constructed and before it runs — so the recorder that forwards is the recorder every forwarding task will clone, and no figure is charged before the forwarding target is in place.

An unshaped session does not size the proxy’s class rows. It has no classes to install, and installing its empty list would mean the first shaped session to arrive afterwards found rows it did not match and charged the default row forever. Nothing is lost by skipping it: an unshaped session’s writers are all behind a configured profile, so it charges nothing at all.

A shaped session cannot be in the same position any more: ShapeProfile::try_new refuses a profile with no classes, so Some(profile) always carries at least one class name to install. ProxyRecorder::adopt_classes keeps its own guard against an empty list all the same, and says there why.

Source

fn proxy_row(&self, class: Class) -> Option<&ClassCounters>

This session’s proxy row for class, when it has a proxy.

Source

pub(crate) fn snapshot(&self) -> ShapeStats

Snapshot every counter for this session.

Allocates: one Vec and one String per class row. Called by a reader (a test, or a control plane), never on the data path.

The flat session totals are computed here from the two legs, which is why no writer maintains them. Two independently written counters can disagree, and the disagreement would surface as a conservation identity that fails for no reason a reader could act on; a sum taken at read time cannot.

Source

pub(crate) fn note_object_seen(&self, side: ProxySide, bytes: u64)

One framed unit entered the shaping path, carrying bytes on the wire.

Two relaxed fetch_adds, called from the object arm of pipe_data_framed behind ForwardCtx::shaping_enabled — so a session with no profile never reaches it, and interest_none.rs stays at an all-zero Counters and an all-zero ShapeStats together.

This is the seen count, not a delivery count: it is taken where the unit becomes visible to the shaper, before any classification or bucket exists to say what became of it. bytes_shaped is therefore the left-hand side of the conservation identity from the start, and the units that add the per-class rows are adding the right-hand side rather than re-defining this one. side is the side the unit arrived on, which the caller already holds as the side it reports every other event for. Charging it here rather than deriving it later is what keeps the downlink stalled separable from “the uplink did”, and it is a side rather than a Direction because the proxy-wide rows need the leg as well — a caller that passed a direction would have thrown away exactly the half of the answer ProxyStats::per_leg exists for.

Source

pub(crate) fn note_unshapeable_seen(&self, side: ProxySide, bytes: u64)

bytes no rule could see entered the shaping path. The sibling of Self::note_object_seen for a unit with no ObjectMeta — a stream header, an oversized object’s passthrough chunk, a bypassed stream’s tail. One relaxed fetch_add, and deliberately not two: objects_seen counts what the classifier saw, and a header is not an object. Bumping it here would make the count that every fixture anchors on (wait until all twelve objects have been classified) depend on how many stream headers happened to arrive first.

bytes_shaped does move, because it is the left-hand side of the conservation identity and the unshapeable row is one of that identity’s right-hand terms. The row itself is charged on release, from the same unit.len().

Source

pub(crate) fn note_dropped(&self, class: Class, bytes: u64)

One unit of bytes was discarded by Overflow::DropTail, charged to the class that claimed it.

No side, because nothing this moves is per leg: a drop is a fact about a rule, and the leg it happened on is already in the difference between the two cells of that direction — the bytes were charged where they arrived and are never charged where they would have left.

Source

pub(crate) fn note_blocked(&self, class: Class)

One episode of the read side being stalled by Overflow::Block began.

Edge-triggered by the caller, which owns the per-stream latch: level counting here would report the runner’s read batching rather than the shaper. Charged to the class of the last unit classified on the stream — the one whose admission filled the queue — because a stall is a property of a stream and a stream has no single class.

Source

pub(crate) fn note_stream_reset_by_shaping(&self, side: ProxySide)

A destination stream carrying traffic that arrived on side was abandoned by a shaping policy.

Charged to the arrival cell, like every other decision figure, and not to the leg the abandoned stream is physically on. What a reader wants from it is which of the two flows the profile gave up on, and the flow is named by where its traffic came from; splitting this one figure the other way would put it in a different cell from the bytes_shaped that explains it.

Source

pub(crate) fn note_delivered(&self, side: ProxySide, class: Class, bytes: u64)

One unit of bytes was released to the destination stream.

The right-hand term the conservation identity was missing: with this producer in place, Σ classes(delivered + dropped) + default + unshapeable == bytes_shaped holds for a stream that ran to completion. It is charged from the same raw.len() note_object_seen took, so the identity is over one measurement rather than two. A teardown flush counts here too. write_all returning is the only definition of released to the destination stream this side of the transport, and the bytes it could not vouch for are separately reported as QueuedBytesAtTeardown — an over-report there is a diagnosable nuisance, a byte missing from both is a hole in the identity.

side is still the side the unit arrived on — every caller holds that one and only that one — and it is the proxy-wide rows that turn it around: a release is the second and last time a unit is measured, on the leg it is leaving by, so ProxyRecorder::departure is what this charges. The session’s own rows are untouched by that, because they have no leg axis to be wrong about.

The proxy’s departure cell counts an object only for a unit that carried one. Class::Unshapeable is the tag every unit no rule could see is pushed with — a stream header, an oversized passthrough chunk — and objects_seen is the classifier’s count on both cells or it is not one figure at all: a header is not an object on the way in and it is not one on the way out.

Source

pub(crate) fn note_tokens_exhausted(&self, class: Class)

One episode of a class’s own bucket being dry began. Edge-triggered by the caller, which owns the per-stream latch, for the same reason Self::note_blocked is: a level count would report how often the release branch woke rather than how often the shaper ran out. Deliberately not bumped when a Discipline is what held the unit back — that is Self::note_starved, and conflating the two would make my bucket is too small and another class is ahead of me one unactionable number.

Source

pub(crate) fn note_starved(&self, class: Class)

One unit waited behind a unit of a different class.

Per unit, once — the queue marks a unit as it charges it. Both causes of head-of-line waiting land here: a discipline holding this class back behind a rival, and a same-stream head of another class that the single per-stream FIFO cannot be reordered around.

Source

pub(crate) fn note_expired(&self, side: ProxySide)

One queued object outlived its clamp under Expiry::ResetStream.

Zero under the default Expiry::Deliver, which delivers the object instead of expiring it — that arm has no producer here and deliberately so.

Charged to side’s arrival cell: the object was read and never written, so the leg it would have left by never carried it.

Source

pub(crate) fn note_mixed_class_stream(&self, side: ProxySide)

One stream carried units of two different classes.

Once per stream, latched by the caller. Head-gating means such a stream’s throughput is decided by whichever class is at its head, so without this count a caller cannot tell configured shaping from head-of-line blocking.

Source

fn row(&self, class: Class) -> &ClassCounters

The row one class’s units are charged to.

A Class::Rule index out of range cannot happen — the indices come from the same profile the rows were pre-sized from — but this is on a forwarding task, where a total function beats a panicking one: an impossible index charges the default row rather than killing the stream.

Source

fn leg(&self, direction: Direction) -> &DirectionCounters

The session totals one leg’s units are charged to.

Trait Implementations§

Source§

impl Debug for ShapeRecorder

Source§

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

Prints the snapshot, not the atomics.

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> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
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, 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, !>

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.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more