Documentation
¶
Overview ¶
Package netem provides an in-process emulated network for testing uTP.
It exists because a congestion controller that passes unit tests tells you nothing. Too aggressive and it floods while claiming to be a background transport; too timid and it gets near-zero throughput. Against loopback both look like "it works". This package makes the difference measurable.
An Endpoint satisfies the utp.Conn interface (ReadFrom/WriteTo/Close), so it drops straight into utp.NewSocket in place of a UDP socket. No root, no network namespaces, no external tooling.
Determinism ¶
Every random decision -- loss, jitter, reordering -- is drawn from a per-link PRNG seeded from Config.Seed, so a single flow over a link makes exactly the same decisions on every run. Two runs of the same test drop the same packets.
Two caveats, stated plainly because a harness that overclaims is worse than none:
- Delivery uses the real clock. Packet-scheduling decisions are reproducible, but the wall-clock instants at which packets actually arrive are subject to Go scheduler noise. Making time itself virtual would require threading an injectable clock through the whole library.
- When several flows share one link, the order in which their WriteTo calls reach the link is genuinely concurrent, so which flow's packet draws which PRNG value can vary between runs. Per-link aggregate behaviour is stable; per-flow drop sequences are not.
Index ¶
- Constants
- Variables
- func FairnessIndex(rates []float64) float64
- func LastSample(samples []utp.ConnectionMetrics) utp.ConnectionMetrics
- func MeasurementRepeats() int
- func MedianFloat(values []float64) float64
- func MinMaxFloat(values []float64) (float64, float64)
- func Quiet() *slog.Logger
- func RunRenoSolo(t *testing.T) float64
- func SplitWhileBothRan(a, b []DeliverySample) (aShare float64, ok bool)
- func WaitForIdle(t *testing.T, mu *sync.Mutex, samples *[]utp.ConnectionMetrics, ...) time.Time
- type BatchedReader
- type Config
- type DeliverySample
- type DeliveryTimeline
- type DriftingClock
- type DropCause
- type Endpoint
- func (e *Endpoint) Addr() *Peer
- func (e *Endpoint) Close() error
- func (e *Endpoint) Name() string
- func (e *Endpoint) PathMTU(utp.ConnectionPeer) (int, bool)
- func (e *Endpoint) ReadFrom(b []byte) (int, utp.ConnectionPeer, error)
- func (e *Endpoint) ReportPathMTU(datagramSize int)
- func (e *Endpoint) Stats() EndpointStats
- func (e *Endpoint) WriteTo(b []byte, dst utp.ConnectionPeer) (int, error)
- func (e *Endpoint) WriteToDontFragment(b []byte, dst utp.ConnectionPeer) (int, error)
- type EndpointStats
- type FlowOptions
- type Link
- type Network
- func (n *Network) AddEndpoint(name string) (*Endpoint, error)
- func (n *Network) Close()
- func (n *Network) Connect(a, b *Endpoint, cfg Config)
- func (n *Network) ConnectAsymmetric(a, b *Endpoint, aToB, bToA Config)
- func (n *Network) ConnectShared(cfg Config, pairs ...[2]*Endpoint)
- func (n *Network) Link(src, dst string) *Link
- func (n *Network) MustAddEndpoint(name string) *Endpoint
- func (n *Network) SetConfig(src, dst string, cfg Config) error
- type Peer
- type QueueSample
- type QueueStats
- type Recorder
- type RenoResult
- type Stats
- type Summary
- type Throughput
- type TransferResult
- type UtpPair
Constants ¶
const ( // DefaultQueueBytes is a bottleneck queue of roughly 64 full packets. DefaultQueueBytes = 64 * 1024 // DefaultReorderDelay is the displacement applied to a reordered packet. DefaultReorderDelay = 20 * time.Millisecond )
const DefaultRecorderLimit = 200_000
DefaultRecorderLimit caps retained samples so a long soak cannot exhaust memory. Once reached, the recorder keeps the first and last halves and drops the middle, which preserves start-up and steady-state behaviour.
const DeferencePayload = 4 * 1024 * 1024
Variables ¶
var ( // ErrClosed is returned by ReadFrom and WriteTo after Close. ErrClosed = errors.New("netem: endpoint closed") // ErrNoRoute is returned when writing to an endpoint with no link. ErrNoRoute = errors.New("netem: no link to destination") // ErrBufferTooSmall is returned when a read buffer cannot hold the packet. ErrBufferTooSmall = errors.New("netem: buffer too small for packet") )
Functions ¶
func FairnessIndex ¶
FairnessIndex returns Jain's fairness index for a set of flow rates: 1.0 is a perfectly even split, 1/n is one flow taking everything. It is the standard way to report how two flows shared a bottleneck.
func LastSample ¶
func LastSample(samples []utp.ConnectionMetrics) utp.ConnectionMetrics
LastSample is the latest of samples, or the zero value if there are none.
func MeasurementRepeats ¶
func MeasurementRepeats() int
MeasurementRepeats is the repeat count, overridable for measurement runs.
func MedianFloat ¶
func MinMaxFloat ¶
func RunRenoSolo ¶
RunRenoSolo measures the competitor with the link to itself.
func SplitWhileBothRan ¶
func SplitWhileBothRan(a, b []DeliverySample) (aShare float64, ok bool)
SplitWhileBothRan is a's share of the bytes the two flows delivered while both were running: from the later flow's first sample to the earlier flow's last. ok is false if they never overlapped or delivered nothing in the overlap.
This is the split itself. A share or a Jain index taken from each flow's whole-transfer goodput is not: a flow squeezed while both run finishes later, with the link to itself for the rest, and its goodput catches up.
func WaitForIdle ¶
func WaitForIdle(t *testing.T, mu *sync.Mutex, samples *[]utp.ConnectionMetrics, since time.Time, limit time.Duration) time.Time
WaitForIdle blocks until the connection has nothing buffered and nothing in flight, and reports when that happened. WaitForIdle blocks until the connection has nothing buffered and nothing in flight, counting only samples taken after since, and reports when that happened.
The `since` argument is not decoration. Without it this returned on the stale pre-transfer sample -- pending, in-flight and queued writes all legitimately zero before anything had been written -- so phase A ended before it began and the window it was supposed to grow never appeared.
Types ¶
type BatchedReader ¶
type BatchedReader struct{ *Endpoint }
BatchedReader tells the socket how many datagrams are already waiting, so it reads them all before acknowledging, as it does from a real socket's batched read (utp.QueuedReader). Endpoint itself does not; see there.
func (BatchedReader) Queued ¶
func (b BatchedReader) Queued() int
type Config ¶
type Config struct {
// Delay is the one-way propagation delay.
Delay time.Duration
// Jitter perturbs Delay by a uniform amount in [-Jitter, +Jitter].
// Delivery order is preserved unless Jitter exceeds packet spacing.
Jitter time.Duration
// LossRate is the independent probability of dropping a packet, in [0,1].
// This is loss on the medium; it is counted separately from queue
// overflow, which is loss caused by congestion.
LossRate float64
// DuplicateRate is the probability that a packet is delivered twice, in
// [0,1]. The copy is made past the bottleneck, as a link-layer
// retransmission or a misbehaving switch would make it: it takes no
// queue space and arrives just after the original.
DuplicateRate float64
// ReorderRate is the probability that a packet is displaced backwards in
// the delivery order by ReorderDelay, in [0,1].
ReorderRate float64
// ReorderDelay is how much extra delay a reordered packet receives.
// Defaults to DefaultReorderDelay.
ReorderDelay time.Duration
// BandwidthBps is the bottleneck rate in bits per second. Zero means
// unlimited: packets are not serialized and never queue.
//
// Packets are serialized at this rate, so a packet's transmission
// occupies len*8/BandwidthBps seconds during which no other packet on
// this link can be sent. Flows sharing a link contend for it.
BandwidthBps uint64
// QueueBytes is the bottleneck queue capacity. A packet arriving when the
// queue is full is tail-dropped. Defaults to DefaultQueueBytes.
//
// This is what bounds standing queueing delay, and therefore what a
// delay-based controller is supposed to keep clear of.
QueueBytes int
// NoQueueSamples stops the link recording a QueueSample for every
// packet. The samples are what QueueSamples reports, for the benchmarks'
// standing-queue figures, and they are kept for the link's life: 40 bytes
// a packet, which a soak of hours turns into gigabytes of the process's
// heap -- 2.5 GB in two hours, all of it the emulator's, which is what
// the soak then measured.
NoQueueSamples bool
// MTU is the largest datagram the path will carry, in bytes. Zero means
// unlimited, which is what every link modelled here did before: any
// datagram, however large, arrived.
//
// A datagram larger than this is dropped outright, which is what an IPv6
// path does, and what an IPv4 path does for a packet marked don't-fragment.
// It is counted separately from loss and from queue overflow, because it is
// neither: it says something about size, and nothing about congestion.
//
// This exists because path-MTU discovery had never been tested against a
// path that limits size. The search converged on whatever ceiling it was
// given, every time, because nothing in the harness could refuse a large
// packet -- so the half of the search that lowers the ceiling was never
// exercised end to end.
MTU int
// OnMTUDrop, when set, is called for every datagram this link refuses for
// being larger than MTU. It is the router that would have forwarded the
// datagram sending an ICMP "fragmentation needed" message back to the
// sender, and it carries what such a message carries: the front of the
// offending datagram and the next-hop MTU.
//
// payload is the whole refused datagram; a real router quotes only its
// front, so a test that wants to model that should truncate. src and dst
// are the endpoint names this link runs between, so a caller can route
// the report back to the socket that sent it. linkMTU is this link's MTU
// expressed the way ICMP expresses it -- the largest IP datagram the hop
// will carry -- which is the emulated link's MTU plus the IP and UDP
// headers a real path would also have had to carry.
//
// It runs on the link's goroutine with no lock held, and must not block.
//
// Without this, an emulated path that refuses a large datagram is
// indistinguishable from one that lost it, so the half of path-MTU
// discovery that takes a router's word for the limit could not be tested
// end to end at all.
OnMTUDrop func(payload []byte, src, dst string, linkMTU int)
// OnOffered, if set, sees every datagram offered to this link, before
// anything decides whether it is carried, dropped or delayed, with the
// instant it was offered. The payload is a copy.
//
// It runs on the sender's goroutine with no lock held, and must not
// block. It exists so a test can see what a sender puts on the wire and
// when -- a retransmission burst is invisible in delivered-packet counts,
// because the burst is exactly what a blackout drops.
OnOffered func(payload []byte, at time.Time)
// FragmentOversized makes this link fragment a datagram larger than MTU
// and forward it, instead of dropping it -- unless the sender set the
// don't-fragment bit, in which case it is dropped as before.
//
// This is the difference between an IPv4 path and an IPv6 one, and it is
// the only condition under which the don't-fragment bit changes anything.
// With it false (the default) this link models IPv6: oversized is always
// dropped, so a path-MTU search learns the truth whether or not it sets
// the bit.
//
// With it true the link models IPv4, where an MTU probe sent without the
// bit is fragmented, arrives, and is acknowledged -- telling the search
// that a size the path cannot actually carry in one piece is fine. That
// is the failure utp.DontFragmentWriter exists to prevent, and this is
// where it can be observed.
FragmentOversized bool
// Seed seeds this link's PRNG. Links in one Network derive distinct seeds
// from Network's seed, so a single Seed makes a whole topology
// reproducible.
Seed int64
}
Config describes one direction of an emulated path. The zero value is a perfect link: no delay, no loss, unlimited bandwidth.
func DeferenceLinkConfig ¶
func DeferenceLinkConfig() Config
DeferenceLinkConfig is the shared bottleneck both experiments run over.
The queue is 256 KB, about 200ms at this rate. It has to be deeper than either controller's delay target for delay-based control to operate at all: on a queue shallower than the target, the queue overflows before the target is reached and a delay-based controller degenerates into a loss-based one, which would make this experiment measure nothing.
type DeliverySample ¶
DeliverySample is a flow's cumulative delivered byte count at a moment.
func ReceivedSeries ¶
func ReceivedSeries(samples []utp.ConnectionMetrics) []DeliverySample
ReceivedSeries converts a receiver's recorded metrics into a delivery series, from the bytes it accepted from its peer.
type DeliveryTimeline ¶
type DeliveryTimeline struct {
// contains filtered or unexported fields
}
DeliveryTimeline records a DeliverySample at most every deliverySampleInterval, and always the latest count.
func (*DeliveryTimeline) Snapshot ¶
func (d *DeliveryTimeline) Snapshot() []DeliverySample
type DriftingClock ¶
DriftingClock wraps a Conn so that the packets leaving it carry timestamps from a clock running at a different rate from the receiver's.
It exists because nothing in this repository could produce clock skew at all. Both endpoints of an emulated network read the same process clock, so every delay measurement was taken between two perfectly synchronised peers -- which is the one condition under which a delay-based congestion controller has no drift problem to solve.
That matters because LEDBAT's whole signal is a one-way delay, and a one-way delay cannot be measured without comparing two clocks. The sender stamps its own clock into every packet; the receiver subtracts that from its own and reports the difference back. If the two clocks run at different rates, the difference contains a systematic error that grows without bound, and the controller reads it as a queue.
libutp is explicit about this, in DelayHist.add_sample:
// The two clocks (in the two peers) are assumed not to
// progress at the exact same rate. They are assumed to be
// drifting, which causes the delay samples to contain
// a systematic error, either they are under-
// estimated or over-estimated. This is why we update the
// delay_base every two minutes, to adjust for this.
(utp_internal.cpp:293-298)
Which direction hurts ¶
A clock running **slow** is the dangerous one. The sender stamps a time that falls further and further behind, so the receiver's `now - stamp` grows, and the growth is reported back to the sender as its own outbound delay. The sender sees a queue that is not there and closes its window.
A clock running fast produces the mirror image, and is harmless: the measured delay shrinks, and the base delay -- a minimum -- follows it down immediately.
What this is not ¶
It models rate error, not offset. A constant offset between two clocks cancels out of LEDBAT entirely, because the controller works in the difference between the current delay and the lowest delay seen, and a constant appears in both. Only a changing offset survives that subtraction, which is why this is expressed in parts per million rather than in milliseconds.
func NewDriftingClock ¶
func NewDriftingClock(conn utp.Conn, ppm float64) *DriftingClock
NewDriftingClock wraps conn with a clock drifting at ppm parts per million. Negative ppm loses time, which is the direction that inflates the peer's view of this sender's delay.
func (*DriftingClock) ReadFrom ¶
func (d *DriftingClock) ReadFrom(b []byte) (int, utp.ConnectionPeer, error)
ReadFrom adjusts the arriving packet's timestamp by the opposite amount, so that the delay this host measures is the delay its own drifting clock would have produced.
**This is half the model, and leaving it out makes the model wrong in a way that is easy to miss.** A host does not have one clock for writing timestamps and another for reading them. If its clock runs slow, the times it stamps into its own packets fall behind *and* the `now - peer_timestamp` it computes for arriving packets shrinks by exactly as much. The first half is what inflates the peer's view of this sender and creates the phantom queue; the second is the only evidence from which clock drift can be told apart from a real queue, and it is what libutp's their_hist watches (utp_internal.cpp:2002-2014).
Modelling only the first half produces a perfectly convincing phantom queue and no way for any correction to notice it. This wrapper did exactly that at first, and the correction built against it did nothing at all -- for the right reason, which took a while to see.
The sign is the mirror of WriteTo's: a clock slow by s stamps `t - s` on the way out, and measures `now - ts` as `true - s` on the way in, which is the same as adding s to the timestamp it reads.
func (*DriftingClock) WriteTo ¶
func (d *DriftingClock) WriteTo(b []byte, dst utp.ConnectionPeer) (int, error)
WriteTo rewrites the outgoing packet's timestamp as the drifting clock would have stamped it, then sends it.
type DropCause ¶
type DropCause int
DropCause records why the link discarded a packet.
const ( // DropLoss is loss on the medium, from Config.LossRate. DropLoss DropCause = iota // DropQueueOverflow is congestion loss: the bottleneck queue was full. DropQueueOverflow // DropReceiverOverflow means the packet arrived but the destination's // inbox was full -- the reader is not keeping up. This models a kernel // receive-buffer overflow, not a network event. DropReceiverOverflow )
type Endpoint ¶
type Endpoint struct {
// contains filtered or unexported fields
}
Endpoint is one host on the emulated network. It satisfies utp.Conn.
func (*Endpoint) PathMTU ¶
func (e *Endpoint) PathMTU(utp.ConnectionPeer) (int, bool)
PathMTU implements utp.PathMTUProvider. It reports the same figure whatever the peer, because ReportPathMTU models a local interface rather than a route.
func (*Endpoint) ReportPathMTU ¶
ReportPathMTU makes this endpoint answer utp.PathMTUProvider with datagramSize, as a host whose local interface is that narrow would.
Opt-in, deliberately. An endpoint that always reported its link's MTU would hand every connection the right answer for free, which is realistic for a host behind a tunnel and useless for testing what happens without one -- the ICMP tests in particular need the search to overshoot before a router can refuse anything.
datagramSize is a uTP datagram size, the same unit Config.MTU is in: this emulated path has one hop, so the link limit and the local interface's limit are the same number.
func (*Endpoint) Stats ¶
func (e *Endpoint) Stats() EndpointStats
Stats returns a snapshot of this endpoint's receive counters.
func (*Endpoint) WriteTo ¶
WriteTo sends a packet towards dst, and satisfies utp.Conn.
It never blocks: the packet is handed to the link, which decides whether it is dropped, queued or delivered. The return value reports bytes accepted for transmission, not bytes delivered -- exactly like a real UDP socket.
func (*Endpoint) WriteToDontFragment ¶
WriteToDontFragment implements utp.DontFragmentWriter: the datagram is sent with fragmentation forbidden, so a link narrower than it drops it instead of fragmenting it.
An emulated network can always honour this, which is the point of having it here: on a real socket the answer depends on the operating system, and a test that could only run where the socket option exists would not run in CI on every platform this library builds for.
Note that it changes nothing unless Config.FragmentOversized is set. Without it a link drops everything over its MTU regardless, which models IPv6.
type EndpointStats ¶
type EndpointStats struct {
PacketsReceived uint64
BytesReceived uint64
// PacketsDroppedFullInbox is packets that arrived while the application
// was not reading fast enough.
PacketsDroppedFullInbox uint64
}
EndpointStats is what an Endpoint observed on its receive side.
type FlowOptions ¶
type FlowOptions struct {
// InitiatorCid is the lower of the two connection ids. Give each
// concurrent flow a distinct value at least 2 apart.
InitiatorCid uint16
// Config, if non-nil, is used as the base connection configuration.
// Its Metrics observer is replaced.
Config *utp.ConnectionConfig
// MetricsInterval controls sampling density.
MetricsInterval time.Duration
// Verify compares received bytes against what was sent. Costs a full
// comparison; leave off for long soaks.
Verify bool
}
FlowOptions configures one transfer.
type Link ¶
type Link struct {
// contains filtered or unexported fields
}
Link is one direction of an emulated path. Traffic from several flows between the same pair of endpoints shares one Link, and therefore contends for its bandwidth and its queue.
func (*Link) QueueDelayPercentile ¶
QueueDelayPercentile returns the p'th percentile (0..1) of observed queueing delay. The median and p95 say far more about a controller's behaviour than the mean, which a long idle tail flatters.
func (*Link) QueueSamples ¶
func (l *Link) QueueSamples() []QueueSample
QueueSamples returns the queueing-delay time series, oldest first. This is the standing-queue measurement a delay-based controller is judged on: LEDBAT is supposed to keep this bounded near its target.
func (*Link) ResetStats ¶
func (l *Link) ResetStats()
ResetStats clears counters and samples, for measuring a steady-state window after warm-up rather than including the handshake.
type Network ¶
type Network struct {
// contains filtered or unexported fields
}
Network is a set of endpoints joined by emulated links.
func NewNetwork ¶
NewNetwork creates an empty network. seed makes the whole topology reproducible: each link derives its own PRNG seed from it.
func (*Network) AddEndpoint ¶
AddEndpoint creates a named endpoint. Names must be unique.
func (*Network) Connect ¶
Connect joins a and b with an independent link in each direction, both using cfg. Use ConnectAsymmetric for different conditions per direction.
func (*Network) ConnectAsymmetric ¶
ConnectAsymmetric joins a and b, applying aToB to traffic from a to b and bToA to traffic from b to a. Real paths are rarely symmetric.
func (*Network) ConnectShared ¶
ConnectShared wires several endpoint pairs through a single bottleneck: one queue, one rate, one loss draw, shared by every pair given.
This is what makes flows actually compete. Connect and ConnectAsymmetric give each directed pair its own Link, so two flows between two different pairs of endpoints each get their own queue and their own full bandwidth -- they run alongside each other without ever contending. A congestion controller cannot be judged against another controller on links like that, because neither one can crowd the other out.
Each element of pairs is a {source, destination} pair, and every one of them is routed through the same Link, in the direction given. To share a bottleneck in both directions, pass both directions.
The Link's stats and queue samples cover all the traffic through it, which is the point: the standing queue at a shared bottleneck is a property of the bottleneck, not of any one flow.
func (*Network) MustAddEndpoint ¶
MustAddEndpoint is AddEndpoint, panicking on error. For tests.
type Peer ¶
type Peer struct {
// contains filtered or unexported fields
}
Peer identifies an Endpoint. It satisfies utp.ConnectionPeer.
type QueueSample ¶
QueueSample is one observation of the bottleneck's state.
type QueueStats ¶
type QueueStats struct {
// contains filtered or unexported fields
}
func RunDeference ¶
func RunDeference(t *testing.T, algo utp.CongestionAlgorithm) (utpMbps, renoMbps float64, queue QueueStats, utpShare float64)
RunDeference runs a uTP flow and the competitor over one shared bottleneck and returns what each achieved, plus the queue they left at the bottleneck.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder collects a connection's metric snapshots into a time series.
Use its Observe method as a utp.ConnectionConfig.Metrics observer. It only appends under a mutex, so it satisfies the requirement that an observer must not block the connection's event loop.
func NewRecorder ¶
func NewRecorder() *Recorder
NewRecorder returns a recorder with the default sample limit.
func (*Recorder) CwndSeries ¶
CwndSeries returns the congestion window over time, for plotting or for asserting that the window actually moves.
func (*Recorder) Last ¶
func (r *Recorder) Last() (utp.ConnectionMetrics, bool)
Last returns the most recent snapshot, and whether there was one.
func (*Recorder) Observe ¶
func (r *Recorder) Observe(m utp.ConnectionMetrics)
Observe records one snapshot. It satisfies utp.MetricsObserver.
func (*Recorder) Samples ¶
func (r *Recorder) Samples() []utp.ConnectionMetrics
Samples returns a copy of the recorded series, oldest first.
type RenoResult ¶
type RenoResult struct {
BytesTransferred int
Elapsed time.Duration
Goodput Throughput
PacketsSent uint64
Retransmits uint64
Timeouts uint64
FastRetransmits uint64
// CwndMaxBytes and CwndMeanBytes describe the congestion window over the
// transfer, sampled once per acknowledgement.
CwndMaxBytes uint32
CwndMeanBytes uint64
// Delivered is the receiver's in-order byte count over time, sampled at
// most every deliverySampleInterval, so a split against another flow can
// be taken over only the time both were running. See SplitWhileBothRan.
Delivered []DeliverySample
}
RenoResult is what a completed Reno flow achieved.
func RunRenoFlow ¶
func RunRenoFlow( ctx context.Context, sender, receiver *Endpoint, payloadBytes int, mss int, ) (*RenoResult, error)
RunRenoFlow sends payloadBytes from sender to receiver over the emulated network and returns what it achieved. It blocks until the transfer completes or ctx is done.
mss is the segment size, excluding this protocol's 8-byte header.
type Stats ¶
type Stats struct {
// Offered is what the sender handed to the link.
PacketsOffered uint64
BytesOffered uint64
// Delivered is what reached the destination's inbox.
PacketsDelivered uint64
BytesDelivered uint64
// Dropped is the total, broken down by cause below.
PacketsDropped uint64
DroppedByLoss uint64
DroppedByQueue uint64
DroppedByReceiver uint64
// DroppedByMTU counts datagrams larger than the link's MTU. This is a
// statement about size, not about congestion, and is kept apart from the
// other two for that reason.
DroppedByMTU uint64
// PacketsFragmented counts datagrams larger than the link's MTU that were
// forwarded anyway, because Config.FragmentOversized is set and the
// sender did not forbid fragmentation. They are delivered, so they are
// not dropped and not counted as such -- the number exists because "the
// probe arrived" and "the probe arrived in one piece" are different
// facts, and a path-MTU search can only tell them apart if something
// records the difference.
PacketsFragmented uint64
PacketsReordered uint64
// PacketsDuplicated counts the extra copies Config.DuplicateRate made.
PacketsDuplicated uint64
QueueDelaySum time.Duration
QueueDelayCount uint64
QueueDelayMax time.Duration
}
Stats is what one Link observed. All counters are cumulative.
func (Stats) MeanQueueDelay ¶
MeanQueueDelay is the average time packets waited for the bottleneck.
func (Stats) MediumLossRate ¶
MediumLossRate is the fraction dropped by Config.LossRate alone, excluding congestion and receiver overflow. This is the number to compare against the configured loss rate.
type Summary ¶
type Summary struct {
Samples int
Span time.Duration
CwndMeanBytes uint64
CwndMaxBytes uint32
CwndMinBytes uint32
// CwndPinnedAtMin is the fraction of samples where the window sat at its
// floor. A controller stuck here has collapsed rather than backed off.
CwndPinnedAtMin float64
RTTMean time.Duration
RTTP50 time.Duration
RTTP95 time.Duration
RTTMax time.Duration
// RTTMin is the lowest round trip seen over the whole flow. On a path
// this flow started on while it was empty, this is the path's real
// unloaded round trip.
RTTMin time.Duration
// StandingQueueP50 and StandingQueueP95 are the round trip above
// RTTMin -- the queue this flow actually left standing on the path.
//
// This exists because QueueingDelay below cannot be trusted for that
// question. QueueingDelay is `PeerTimestampDiff - BaseDelay`, and BaseDelay is
// the *controller's own estimate* of the empty path. A delay-based
// controller whose base-delay estimate has drifted upwards -- which is
// precisely the failure LEDBAT++ exists to fix -- reports a small
// queueing delay while sitting on a large queue, because it is
// subtracting the queue from itself.
//
// Measured on a 40ms path: classic LEDBAT reported 156µs of queueing
// delay while its round trip sat at 80ms. It was causing 40ms of queue
// and could not see it. StandingQueueP50 reports the 40ms.
StandingQueueP50 time.Duration
StandingQueueP95 time.Duration
// Queueing delay is the excess of measured one-way delay over the lowest
// seen: the standing queue LEDBAT exists to bound.
QueueingDelayMean time.Duration
QueueingDelayP50 time.Duration
QueueingDelayP95 time.Duration
QueueingDelayMax time.Duration
BytesSent uint64
PacketsSent uint64
PacketsRetransmitted uint64
RetransmitRate float64
Timeouts uint64
FastRetransmits uint64
}
Summary condenses a recorded series into the numbers the gates report.
type Throughput ¶
Throughput is a goodput measurement over a wall-clock interval.
func (Throughput) Mbps ¶
func (t Throughput) Mbps() float64
Mbps returns the rate in megabits per second.
func (Throughput) String ¶
func (t Throughput) String() string
String renders the throughput for a test log.
type TransferResult ¶
type TransferResult struct {
// BytesTransferred is what the receiver actually read.
BytesTransferred int
// Elapsed is wall-clock time from connect to end of stream.
Elapsed time.Duration
// Goodput is application-level throughput: payload delivered per second,
// excluding headers and retransmissions. This is the number that matters
// -- a flow can be busy and still deliver nothing.
Goodput Throughput
// Sender and Receiver hold the recorded metric series for each side.
Sender *Recorder
Receiver *Recorder
// Verified reports whether the received bytes matched what was sent.
Verified bool
}
TransferResult is what one flow achieved.
func (*TransferResult) String ¶
func (r *TransferResult) String() string
String renders the result for a test log.
type UtpPair ¶
UtpPair is two uTP sockets joined by an emulated network, ready to carry flows.
func NewUtpPair ¶
NewUtpPair wraps two endpoints in uTP sockets. Close the Network to shut everything down.
func (*UtpPair) RunTransfer ¶
func (p *UtpPair) RunTransfer(ctx context.Context, data []byte, opts FlowOptions) (*TransferResult, error)
RunTransfer sends data from the pair's A side to its B side and waits for the receiver to read it to end of stream.
A and B here are the *directions of data flow*, not the endpoints' names: the sender initiates, the receiver accepts.