fsp-server: stop clamping GET_FILE replies to 1024, which capped streaming
Streaming measured 509 KB/s, BELOW the menu's 850 KB/s TCP dump, and the
arithmetic says exactly why.
I had this wrong: QUEUE_SIZE 2 is the number of queued GAME reads, not
concurrent network requests. fsp_read_queued() sends ONE fsp_get_file and
arms a 1 s alarm; fsp_pop_queue() issues the next only once the response
arrives. Streaming is therefore STRICTLY ONE REQUEST IN FLIGHT, so
throughput = bytes_per_reply / round_trip
The server clamped replies to FSP_SPACE (1024), giving 1024 B / ~2.01 ms =
509 KB/s -- the measured figure almost exactly. But the in-game client asks
for pmtu - 40 (1460 at a 1500 MTU) and accepts it; only fsplib has the 1024
limit, and fsplib never asks for more than that. So honouring the requested
size is safe for both clients, and at 1440 B (the 1500 MTU budget rounded
down to 32) the same round trip gives ~1.41x, about 700 KB/s.
Replies stay a multiple of 32 unless they are the file tail, because the
client computes data_size = MIN(requested, OSRoundDown32B(reply_len)) and a
non-aligned mid-file reply would be silently truncated.
Also instruments the round trip: with one request in flight the observed
interval IS the RTT, so the report now splits it into total and this server's
median service time and says SERVER-bound or console-bound. Without that,
"it is slow" has no owner. And the raw socket now binds ETH_P_IP rather than
ETH_P_ALL, so our own outgoing frames are no longer copied to userspace and
parsed in Python on the path we are trying to shorten.
T13 checks a 1460-byte request yields 1440 (32-aligned) while an fsplib-sized
1024 request still yields exactly 1024.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-4
@@ -64,8 +64,12 @@ class RawTransport:
|
||||
|
||||
def __init__(self, iface, port):
|
||||
self.port = port
|
||||
# ETH_P_IP, not ETH_P_ALL: with ALL, every frame on the link --
|
||||
# including our own outgoing ones -- is copied to userspace and
|
||||
# parsed in Python, which adds latency to the very round trip we are
|
||||
# trying to shorten.
|
||||
self.s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
|
||||
socket.htons(ETH_P_ALL))
|
||||
socket.htons(ETH_P_IP))
|
||||
self.s.bind((iface, 0))
|
||||
self.our_mac = self.s.getsockname()[4][:6]
|
||||
|
||||
@@ -73,7 +77,7 @@ class RawTransport:
|
||||
"""Return (payload, reply_ctx) for a UDP datagram aimed at our port."""
|
||||
while True:
|
||||
frame = self.s.recv(65535)
|
||||
if len(frame) < 34 or struct.unpack("!H", frame[12:14])[0] != ETH_P_IP:
|
||||
if len(frame) < 34:
|
||||
continue
|
||||
ip = frame[14:]
|
||||
ihl = (ip[0] & 0x0F) * 4
|
||||
@@ -261,6 +265,14 @@ def main():
|
||||
"which is what made mini-dhcpd work.")
|
||||
ap.add_argument("--bind", default="0.0.0.0")
|
||||
ap.add_argument("--port", type=int, default=21)
|
||||
ap.add_argument("--max-reply", type=int, default=1440,
|
||||
help="largest GET_FILE payload, 32-ALIGNED (default 1440 "
|
||||
"= 1500 MTU - 20 IP - 8 UDP - 12 FSP, rounded down "
|
||||
"to 32). Streaming is strictly ONE request in "
|
||||
"flight, so throughput is this divided by the round "
|
||||
"trip: at ~2 ms, 1024 B gives ~509 KB/s and 1440 B "
|
||||
"gives ~700 KB/s. fsplib never asks for more than "
|
||||
"its own FSP_SPACE (1024) so the menu is unaffected.")
|
||||
ap.add_argument("--quiet-reads", action="store_true",
|
||||
help="only log the rate, not every read")
|
||||
ap.add_argument("--trace", action="store_true",
|
||||
@@ -298,6 +310,7 @@ def main():
|
||||
print(f" {pkt_[:48].hex()}")
|
||||
|
||||
uploads = {} # peer -> staged bytes, committed by CC_INSTALL
|
||||
service = [] # per-request service time, to split the round trip
|
||||
served = 0
|
||||
t_first = None
|
||||
t_report = time.time()
|
||||
@@ -467,7 +480,18 @@ def main():
|
||||
|
||||
if cmd == CC_GET_FILE:
|
||||
want = struct.unpack("!H", r["extra"][:2])[0] if len(r["extra"]) >= 2 else 1024
|
||||
want = min(want, FSP_SPACE)
|
||||
# ⚠️ Do NOT clamp to FSP_SPACE (1024). The in-game client asks
|
||||
# for pmtu - 40 (1460 at a 1500 MTU) and accepts it; only
|
||||
# fsplib has the 1024 limit, and fsplib never asks for more than
|
||||
# that, so honouring the request is safe for both. Clamping here
|
||||
# is what held streaming to 509 KB/s: one request in flight means
|
||||
# throughput is payload / RTT, nothing else.
|
||||
want = min(want, a.max_reply)
|
||||
# Keep it a multiple of 32 unless it is the file tail: the client
|
||||
# does data_size = MIN(requested, OSRoundDown32B(reply_len)), so a
|
||||
# non-aligned mid-file reply is silently truncated.
|
||||
if want >= 32:
|
||||
want &= ~31
|
||||
if fh_path != real:
|
||||
if fh:
|
||||
fh.close()
|
||||
@@ -478,12 +502,14 @@ def main():
|
||||
reply(ctx, pack(CC_ERR, SERVER_KEY, seq, pos,
|
||||
f"{e}\0".encode()), "ERR")
|
||||
continue
|
||||
t_req = time.time()
|
||||
fh.seek(pos)
|
||||
data = fh.read(want)
|
||||
reply(ctx, pack(CC_GET_FILE, SERVER_KEY, seq, pos, data), "GET_FILE")
|
||||
served += len(data)
|
||||
reads += 1
|
||||
now = time.time()
|
||||
service.append(now - t_req)
|
||||
if t_first is None:
|
||||
t_first = now
|
||||
print(f"[{peer[0]}] GET_FILE {path!r} streaming started")
|
||||
@@ -492,9 +518,18 @@ def main():
|
||||
f"want={want} -> {len(data)} B")
|
||||
if now - t_report >= 2.0:
|
||||
el = max(now - t_first, 1e-9)
|
||||
svc = sorted(service)
|
||||
med = svc[len(svc) // 2] * 1000 if svc else 0.0
|
||||
rtt = (el / reads) * 1000 if reads else 0.0
|
||||
print(f" streaming {served/el/1024:7.0f} KB/s "
|
||||
f"{served/2**20:8.1f} MiB in {el:6.1f}s "
|
||||
f"{reads/el:5.0f} reads/s last pos {pos:#x}")
|
||||
f"{reads/el:5.0f} reads/s {len(data)} B/reply")
|
||||
# One request in flight, so the observed interval IS the round
|
||||
# trip. Splitting it shows whether WE are the slow part.
|
||||
print(f" round trip {rtt:5.2f} ms, of which this server "
|
||||
f"{med:5.2f} ms (median) -> "
|
||||
f"{'SERVER-bound' if med > rtt/2 else 'console-bound'}")
|
||||
service.clear()
|
||||
t_report = now
|
||||
continue
|
||||
|
||||
|
||||
Reference in New Issue
Block a user