throughput: the upstream direction, counted by the only party that can
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 not the question being asked. A new wire type the server counts and deliberately never answers: 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 instead of inheriting the first. Same honesty rule as downstream: measures_network is false when what arrived matches what was offered, because then the path was never the constraint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8646bab52d
commit
892e952a8e
@@ -109,6 +109,49 @@ class ProbeSession(
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends paced upstream traffic for [durationMs] and reports what was put on the wire.
|
||||
*
|
||||
* Paced rather than flat out, for the same reason the server paces: an unpaced burst measures
|
||||
* the local 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 accumulates the
|
||||
* scheduler's error and drifts the achieved rate below target over a multi-second run.
|
||||
*
|
||||
* Nothing comes back — the server counts and stays silent — so the result here is only the
|
||||
* send side. The measurement is the gap between this and the server's tally.
|
||||
*/
|
||||
fun sendThroughput(durationMs: Long, kbps: Int, sizeBytes: Int = 1200): Sent {
|
||||
val size = sizeBytes.coerceIn(Wire.HEADER_SIZE + 16, 1472)
|
||||
val payload = ByteArray(size - Wire.HEADER_SIZE)
|
||||
val perPacketNs = (size.toLong() * 8 * 1_000_000 / kbps.coerceAtLeast(1)).coerceAtLeast(1_000)
|
||||
|
||||
val start = System.nanoTime()
|
||||
val deadline = start + durationMs * 1_000_000
|
||||
var next = start
|
||||
var packets = 0
|
||||
var bytes = 0L
|
||||
while (System.nanoTime() < deadline) {
|
||||
val pkt = Wire.build(Wire.TYPE_THROUGHPUT_UP, prefix, ++seq, nowNs(), key, payload)
|
||||
try {
|
||||
socket.send(DatagramPacket(pkt, pkt.size, server))
|
||||
} catch (e: java.io.IOException) {
|
||||
// A local send failure is our condition, not the path's. Stop and report what
|
||||
// actually left, rather than counting the remainder as loss on the network.
|
||||
break
|
||||
}
|
||||
packets++
|
||||
bytes += pkt.size
|
||||
next += perPacketNs
|
||||
val sleepNs = next - System.nanoTime()
|
||||
if (sleepNs > 0) Thread.sleep(sleepNs / 1_000_000, (sleepNs % 1_000_000).toInt())
|
||||
}
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
return Sent(packets, bytes, elapsedMs, if (elapsedMs > 0) (bytes * 8 / elapsedMs).toInt() else 0)
|
||||
}
|
||||
|
||||
/** What one upstream run put on the wire locally. */
|
||||
data class Sent(val packets: Int, val bytes: Long, val durationMs: Long, val kbps: Int)
|
||||
|
||||
/** One packet received from the server, with the wire size actually delivered. */
|
||||
data class Received(val type: Int, val seq: Int, val sizeBytes: Int, val tRxNs: Long)
|
||||
|
||||
|
||||
@@ -41,6 +41,13 @@ object Wire {
|
||||
/** One packet of a sustained-rate downstream run. */
|
||||
const val TYPE_THROUGHPUT_DATA: Int = 0x0E
|
||||
|
||||
/**
|
||||
* One packet of a client-driven upstream run. The server counts it and does not answer:
|
||||
* a reply would double the traffic and drag the return path into a measurement that is
|
||||
* specifically about the outbound one.
|
||||
*/
|
||||
const val TYPE_THROUGHPUT_UP: Int = 0x0F
|
||||
|
||||
/** The 8-byte on-the-wire prefix = first 16 hex chars of the session id, decoded. */
|
||||
fun wirePrefix(sessionId: String): ByteArray {
|
||||
require(sessionId.length >= 16) { "session id too short" }
|
||||
|
||||
Reference in New Issue
Block a user