netem

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 16 Imported by: 0

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

View Source
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
)
View Source
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.

View Source
const DeferencePayload = 4 * 1024 * 1024

Variables

View Source
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

func FairnessIndex(rates []float64) float64

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 MedianFloat(values []float64) float64

func MinMaxFloat

func MinMaxFloat(values []float64) (float64, float64)

func Quiet

func Quiet() *slog.Logger

Quiet is a logger that drops everything a test would not act on.

func RunRenoSolo

func RunRenoSolo(t *testing.T) float64

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

type DeliverySample struct {
	At    time.Time
	Bytes uint64
}

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) Record

func (d *DeliveryTimeline) Record(at time.Time, bytes uint64)

func (*DeliveryTimeline) Snapshot

func (d *DeliveryTimeline) Snapshot() []DeliverySample

type DriftingClock

type DriftingClock struct {
	utp.Conn
	// contains filtered or unexported fields
}

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
)

func (DropCause) String

func (d DropCause) String() string

String names the drop cause.

type Endpoint

type Endpoint struct {
	// contains filtered or unexported fields
}

Endpoint is one host on the emulated network. It satisfies utp.Conn.

func (*Endpoint) Addr

func (e *Endpoint) Addr() *Peer

Addr returns this endpoint's peer identity, for use as a uTP destination.

func (*Endpoint) Close

func (e *Endpoint) Close() error

Close shuts the endpoint down. It is idempotent.

func (*Endpoint) Name

func (e *Endpoint) Name() string

Name returns this endpoint's name.

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) ReadFrom

func (e *Endpoint) ReadFrom(b []byte) (int, utp.ConnectionPeer, error)

ReadFrom blocks until a packet arrives, and satisfies utp.Conn.

func (*Endpoint) ReportPathMTU

func (e *Endpoint) ReportPathMTU(datagramSize int)

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

func (e *Endpoint) WriteTo(b []byte, dst utp.ConnectionPeer) (int, error)

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

func (e *Endpoint) WriteToDontFragment(b []byte, dst utp.ConnectionPeer) (int, error)

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

func (l *Link) QueueDelayPercentile(p float64) time.Duration

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.

func (*Link) Stats

func (l *Link) Stats() Stats

Stats returns a snapshot of this link's counters.

type Network

type Network struct {
	// contains filtered or unexported fields
}

Network is a set of endpoints joined by emulated links.

func NewNetwork

func NewNetwork(seed int64) *Network

NewNetwork creates an empty network. seed makes the whole topology reproducible: each link derives its own PRNG seed from it.

func (*Network) AddEndpoint

func (n *Network) AddEndpoint(name string) (*Endpoint, error)

AddEndpoint creates a named endpoint. Names must be unique.

func (*Network) Close

func (n *Network) Close()

Close shuts down every link and endpoint.

func (*Network) Connect

func (n *Network) Connect(a, b *Endpoint, cfg Config)

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

func (n *Network) ConnectAsymmetric(a, b *Endpoint, aToB, bToA Config)

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

func (n *Network) ConnectShared(cfg Config, pairs ...[2]*Endpoint)

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 (n *Network) Link(src, dst string) *Link

Link returns the link carrying traffic from src to dst, or nil.

func (*Network) MustAddEndpoint

func (n *Network) MustAddEndpoint(name string) *Endpoint

MustAddEndpoint is AddEndpoint, panicking on error. For tests.

func (*Network) SetConfig

func (n *Network) SetConfig(src, dst string, cfg Config) error

SetConfig replaces the configuration of the src->dst link while it is running, for tests that change conditions mid-transfer. In-flight packets keep the schedule they were already given.

type Peer

type Peer struct {
	// contains filtered or unexported fields
}

Peer identifies an Endpoint. It satisfies utp.ConnectionPeer.

func (*Peer) Hash

func (p *Peer) Hash() string

Hash returns the peer's identity.

func (*Peer) Name

func (p *Peer) Name() string

Name returns the peer's name.

func (*Peer) String

func (p *Peer) String() string

String returns the peer's name.

type QueueSample

type QueueSample struct {
	At           time.Time
	Delay        time.Duration
	BacklogBytes int
}

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

func (r *Recorder) CwndSeries() ([]time.Duration, []uint32)

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) Len

func (r *Recorder) Len() int

Len returns how many samples have been recorded.

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.

func (*Recorder) Summary

func (r *Recorder) Summary() Summary

Summary condenses the recorded series.

Samples taken before the connection is established carry no controller state, so they are excluded from the congestion statistics; including them would drag every average towards zero.

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) LossRate

func (s Stats) LossRate() float64

LossRate is the fraction of offered packets that did not arrive.

func (Stats) MeanQueueDelay

func (s Stats) MeanQueueDelay() time.Duration

MeanQueueDelay is the average time packets waited for the bottleneck.

func (Stats) MediumLossRate

func (s Stats) MediumLossRate() float64

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.

func (Stats) String

func (s Stats) String() string

String renders the stats for a test log.

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.

func (Summary) String

func (s Summary) String() string

String renders the summary for a test log.

type Throughput

type Throughput struct {
	Bytes    uint64
	Duration time.Duration
}

Throughput is a goodput measurement over a wall-clock interval.

func (Throughput) Bps

func (t Throughput) Bps() float64

Bps returns the rate in bits per second.

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

type UtpPair struct {
	Net          *Network
	A, B         *Endpoint
	SockA, SockB *utp.Socket
}

UtpPair is two uTP sockets joined by an emulated network, ready to carry flows.

func NewUtpPair

func NewUtpPair(ctx context.Context, n *Network, a, b *Endpoint, logger *slog.Logger) *UtpPair

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址