engine: live test for upstream throughput
3125 sent, 3125 counted by the server, 0% loss. The assertion that earns its keep is received <= sent: that is what catches a counter that was never reset between runs, which would otherwise look like a suspiciously good result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
892e952a8e
commit
d04babff51
@@ -941,3 +941,27 @@ Also: a `BackHandler` now returns from Settings/History to the run screen. The s
|
|||||||
state variable with nothing connecting it to the back stack, so the system Back gesture left the
|
state variable with nothing connecting it to the back stack, so the system Back gesture left the
|
||||||
app entirely. Enabled only when there is somewhere to go back to, so Back still exits from the run
|
app entirely. Enabled only when there is somewhere to go back to, so Back still exits from the run
|
||||||
screen.
|
screen.
|
||||||
|
|
||||||
|
### Upstream throughput (server-v0.6.3, 2026-08-01)
|
||||||
|
The mirror of the downstream case: the client generates the traffic and the server counts it. No
|
||||||
|
grant is involved — the client is sending its own packets, so there is nothing to amplify — but it
|
||||||
|
does need the server's tally, because **only the far end knows how much arrived**. Without that
|
||||||
|
number a sender measures how fast it can *transmit*, which is usually just the speed of the local
|
||||||
|
NIC and is a different question from the one being asked.
|
||||||
|
|
||||||
|
`TYPE_THROUGHPUT_UP` (0x0F) is counted and deliberately **never answered**: a reply would double
|
||||||
|
the traffic and drag the return path into a measurement that is specifically about the outbound
|
||||||
|
one.
|
||||||
|
|
||||||
|
The tally is a counter, not a list, and short-circuits **before** the observation log. A
|
||||||
|
five-second run at 20 Mbps is around ten thousand packets; one struct each would turn a
|
||||||
|
measurement into an allocation storm on a shared server, and nothing needs the per-packet detail
|
||||||
|
since the client holds the send-side record. The gap between the two counts is the loss.
|
||||||
|
|
||||||
|
`direction=up` on the throughput action sends nothing — it zeroes the counter, so a second run in
|
||||||
|
one session measures itself rather than inheriting the first one's packets. The live test asserts
|
||||||
|
`received <= sent`, which is what catches a counter that was never reset.
|
||||||
|
|
||||||
|
Live against fmr: **3125 sent, 3125 counted, 0 % loss, 10.0 Mbit/s** at a 10 Mbit/s request, with
|
||||||
|
`measures_network: false` — correct, since what arrived matched what was offered, so the path was
|
||||||
|
never the constraint.
|
||||||
|
|||||||
@@ -65,4 +65,41 @@ class LiveThroughputTest {
|
|||||||
assertTrue(m.contains(""""limited_by":"duration""""),
|
assertTrue(m.contains(""""limited_by":"duration""""),
|
||||||
"the run did not end on the clock, so the rate measures the server, not the path: $m")
|
"the run did not end on the clock, so the rate measures the server, not the path: $m")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upstream is the direction only the far end can measure. The assertion that matters is that
|
||||||
|
// the server's count is present and plausible against what we sent — a test that only checked
|
||||||
|
// "we transmitted some Mbps" would pass against a server that counted nothing at all.
|
||||||
|
@Test
|
||||||
|
fun measuresUpstreamAgainstTheServersCount() {
|
||||||
|
if (url == null || pin == null || cred == null || udp == null) {
|
||||||
|
println("LiveThroughputTest(up) skipped"); return
|
||||||
|
}
|
||||||
|
val control = ControlClient(url, setOf(pin), "0.2.0")
|
||||||
|
val session = control.createSession(cred, target)
|
||||||
|
val (host, port) = udp.split(":").let { it[0] to it[1].toInt() }
|
||||||
|
|
||||||
|
val (test, findings) = ProbeSession(cred, session, host, port).use { ps ->
|
||||||
|
ps.echo()
|
||||||
|
ThroughputMeasurement(SystemIdSource()).runUpstream(
|
||||||
|
cred, session.sessionId, control, ps, sessionRef = "sess-1",
|
||||||
|
durationS = 3, kbps = 10_000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
control.deleteSession(cred, session.sessionId)
|
||||||
|
|
||||||
|
val m = assertNotNull(test.metrics).toString()
|
||||||
|
println("upstream: ${test.status} $m")
|
||||||
|
for (f in findings) println("finding ${f.code} [${f.severity}] ${f.title}")
|
||||||
|
|
||||||
|
assertEquals(TestStatus.OK, test.status, "the server counted nothing: $m")
|
||||||
|
val recv = Regex(""""received_packets":(\d+)""").find(m)?.groupValues?.get(1)?.toInt()
|
||||||
|
val sent = Regex(""""sent_packets":(\d+)""").find(m)?.groupValues?.get(1)?.toInt()
|
||||||
|
assertNotNull(recv); assertNotNull(sent)
|
||||||
|
assertTrue(sent > 100, "barely anything was sent, so the rate means nothing: $m")
|
||||||
|
assertTrue(recv > 0, "the server received none of $sent packets: $m")
|
||||||
|
// The counts should be close on a healthy path; wildly different means the two sides are
|
||||||
|
// counting different things rather than the network losing packets.
|
||||||
|
assertTrue(recv <= sent, "the server counted MORE than we sent — the counter is not being reset")
|
||||||
|
println("sent $sent, server saw $recv")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user