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: boolWhether 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
impl ShapeRecorder
Sourcepub(crate) fn for_profile(profile: Option<&ShapeProfile>) -> Self
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.
Sourcepub(crate) fn attached(
profile: Option<&ShapeProfile>,
proxy: Arc<ProxyRecorder>,
) -> Self
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.
Sourcefn proxy_row(&self, class: Class) -> Option<&ClassCounters>
fn proxy_row(&self, class: Class) -> Option<&ClassCounters>
This session’s proxy row for class, when it has a proxy.
Sourcepub(crate) fn snapshot(&self) -> ShapeStats
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.
Sourcepub(crate) fn note_object_seen(&self, side: ProxySide, bytes: u64)
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.
Sourcepub(crate) fn note_unshapeable_seen(&self, side: ProxySide, bytes: u64)
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().
Sourcepub(crate) fn note_dropped(&self, class: Class, bytes: u64)
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.
Sourcepub(crate) fn note_blocked(&self, class: Class)
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.
Sourcepub(crate) fn note_stream_reset_by_shaping(&self, side: ProxySide)
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.
Sourcepub(crate) fn note_delivered(&self, side: ProxySide, class: Class, bytes: u64)
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.
Sourcepub(crate) fn note_tokens_exhausted(&self, class: Class)
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.
Sourcepub(crate) fn note_starved(&self, class: Class)
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.
Sourcepub(crate) fn note_expired(&self, side: ProxySide)
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.
Sourcepub(crate) fn note_mixed_class_stream(&self, side: ProxySide)
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.
Sourcefn row(&self, class: Class) -> &ClassCounters
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.
Sourcefn leg(&self, direction: Direction) -> &DirectionCounters
fn leg(&self, direction: Direction) -> &DirectionCounters
The session totals one leg’s units are charged to.