engine: split packet loss by direction using the server's observations
"3 % loss" sends an engineer looking in both directions at once. The server records every packet it received per sequence number, so the two cases are distinguishable: sent-but-never-seen is upstream loss, seen-but-no-reply is downstream. The findings say which, and say what is not implicated. Downstream loss is measured against what reached the server, not against what was sent — the other denominator counts every upstream loss twice and overstates the return path. Per-direction jitter comes out of the same records without needing synchronised clocks: (server_rx - client_tx) carries a constant unknown offset, and differencing successive samples cancels it, so RFC 3393 variation is honestly attributable to a direction even though absolute latency is not. Correlation is by wire sequence number, not loop index — the counter is shared with every packet type on the session. ProbeSession exposes it even for a lost probe, since that is precisely the packet whose direction is in question. Live against fmr: 0.08 ms upstream jitter vs 0.85 ms downstream, an asymmetry a round-trip test cannot see. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3e7e3b8d33
commit
4ffa6e4ae2
@@ -44,13 +44,26 @@ class ProbeSession(
|
||||
*/
|
||||
fun echo(paddingBytes: Int = 40): EchoResult? {
|
||||
val t0 = System.nanoTime()
|
||||
val pkt = Wire.build(Wire.TYPE_ECHO_REQ, prefix, ++seq, nowNs(), key, ByteArray(paddingBytes))
|
||||
val wireSeq = ++seq
|
||||
val pkt = Wire.build(Wire.TYPE_ECHO_REQ, prefix, wireSeq, nowNs(), key, ByteArray(paddingBytes))
|
||||
socket.send(DatagramPacket(pkt, pkt.size, server))
|
||||
// A lost probe still has a sequence number, and that number is what lets the server's
|
||||
// observations say whether it was lost going out or coming back — so report it either way.
|
||||
lastSeq = wireSeq
|
||||
val resp = receive(Wire.TYPE_ECHO_RESP) ?: return null
|
||||
val rttMs = (System.nanoTime() - t0) / 1_000_000.0
|
||||
return EchoResult(rttMs, Observation.parse(resp.payload))
|
||||
return EchoResult(rttMs, Observation.parse(resp.payload), wireSeq)
|
||||
}
|
||||
|
||||
/**
|
||||
* The wire sequence number of the most recent [echo], including one that was lost.
|
||||
*
|
||||
* Exposed because the caller cannot derive it: the counter is shared with every other packet
|
||||
* type on this session, so "the nth echo" is not "sequence n".
|
||||
*/
|
||||
var lastSeq: Int = 0
|
||||
private set
|
||||
|
||||
/** One MTU probe of [totalSize] bytes (DF is set by the OS on the socket where supported).
|
||||
* Returns the size the server acknowledged receiving, or null if the probe was lost. */
|
||||
fun mtuProbe(totalSize: Int): Int? {
|
||||
@@ -112,5 +125,5 @@ class ProbeSession(
|
||||
|
||||
override fun close() = socket.close()
|
||||
|
||||
data class EchoResult(val rttMs: Double, val observation: Observation?)
|
||||
data class EchoResult(val rttMs: Double, val observation: Observation?, val seq: Int = 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user