throughput: paced downstream rate, with the qualifier that makes it honest
A throughput number reports the smallest limit on the path, and the sender's own ceiling is one of the candidates. If the server was asked for 50 Mbps and 50 Mbps arrived, the network was never the constraint and "50 Mbps" says nothing about it. So the result always carries limited_by and measures_network, and a finding is raised only when the path is actually implicated. Loss is computed against the *sender's* count, not the requested rate: the server reports what it put on the wire, and the gap is the loss. A receiver alone cannot tell "the network dropped it" from "the sender never sent it", and guessing turns a healthy server-side limit into a phantom network fault. The count is stored per action, not per packet — half a million packets of structs would turn a measurement into memory exhaustion. Sending is paced rather than flat out. An unpaced burst measures the server's NIC and the first queue it meets, then collapses into loss that reads as a network fault. The schedule is absolute rather than sleep-per-packet, which would accumulate scheduler error and drift the rate down over a ten-second run. Throughput gets its own grant budget sized from the request, so every other action stays bounded at 8 MiB. When the byte cap binds before the clock does, the *duration* is shortened and reported, rather than the run being truncated halfway: promising thirty seconds and delivering twenty-one is the same information with a surprise attached, and it keeps "the clock ended the run" as the normal case — the only case where the rate is a clean property of the path. That last behaviour came out of a test that failed honestly: 30 s at 100 Mbps needs 375 MB against a 256 MB cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
35744c609e
commit
3333788d9e
@@ -40,6 +40,7 @@ type Session struct {
|
||||
packetsSeen uint64
|
||||
udpObs []UDPObservation // ring, newest last, cap obsCap
|
||||
connectBack []ConnectBackResult
|
||||
throughput []ThroughputReport
|
||||
}
|
||||
|
||||
const obsCap = 4096
|
||||
@@ -54,6 +55,18 @@ type UDPObservation struct {
|
||||
Type uint8 `json:"type"`
|
||||
}
|
||||
|
||||
// ThroughputReport is the server's own account of a sustained send: what it managed to put on
|
||||
// the wire, and what stopped it. The client needs this to interpret its own count — the gap
|
||||
// between the two IS the loss, and without the sender's number a receiver can only guess.
|
||||
type ThroughputReport struct {
|
||||
ActionID string `json:"action_id"`
|
||||
Packets int `json:"packets"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Kbps int `json:"kbps"`
|
||||
LimitedBy string `json:"limited_by"`
|
||||
}
|
||||
|
||||
// ConnectBackResult records one connect-back action outcome.
|
||||
type ConnectBackResult struct {
|
||||
ActionID string `json:"action_id"`
|
||||
@@ -87,6 +100,27 @@ func (s *Session) Observations() (packetsSeen uint64, udp []UDPObservation, cb [
|
||||
append([]ConnectBackResult(nil), s.connectBack...)
|
||||
}
|
||||
|
||||
// RecordThroughput stores the server's account of one sustained send.
|
||||
//
|
||||
// Kept as a per-action summary rather than per-packet records: a ten-second run at 50 Mbps is
|
||||
// half a million packets, and holding one struct each would turn a measurement into a memory
|
||||
// exhaustion. The client has the per-packet view; the server only needs to say how many it sent.
|
||||
func (s *Session) RecordThroughput(actionID string, packets int, bytes, durationMs int64, kbps int, limitedBy string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.throughput = append(s.throughput, ThroughputReport{
|
||||
ActionID: actionID, Packets: packets, Bytes: bytes,
|
||||
DurationMs: durationMs, Kbps: kbps, LimitedBy: limitedBy,
|
||||
})
|
||||
}
|
||||
|
||||
// ThroughputReports returns the server's account of every sustained send in this session.
|
||||
func (s *Session) ThroughputReports() []ThroughputReport {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]ThroughputReport(nil), s.throughput...)
|
||||
}
|
||||
|
||||
// DataSource returns the last verified data-plane source (invalid when the
|
||||
// session has not sent data-plane traffic yet).
|
||||
func (s *Session) DataSource() netip.AddrPort {
|
||||
|
||||
Reference in New Issue
Block a user