fsp-server: report capability separately from duty cycle
The rate line was bytes/elapsed, which is not the capability of the path: the
game reads in bursts, so any window containing idle time understates what the
link can do. Dennis spotted this -- 593 KB/s observed while the per-round-trip
arithmetic says ~700 -- and the fix belongs in the tool, not in the reader's
head.
Now reports, every two seconds:
average bytes / elapsed, including idle (what the game happened to want)
CAPABLE bytes / time ACTUALLY serving (what the path can do)
duty fraction of the window streaming
round trip median gap, split into total and this server's service time,
plus the implied ceiling (reply_size / RTT) at that round trip
Consecutive requests closer together than 10 ms count as streaming (the round
trip is ~2 ms); longer gaps are the game not asking and are excluded from the
capability figure. A synthetic burst model confirms the split: 1440 B every
2 ms in bursts with 200 ms idle reports 352 KB/s average but 703 KB/s capable
at 50% duty, which is the distinction that matters when asking whether the
adapter is the limit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+39
-9
@@ -311,6 +311,17 @@ def main():
|
|||||||
|
|
||||||
uploads = {} # peer -> staged bytes, committed by CC_INSTALL
|
uploads = {} # peer -> staged bytes, committed by CC_INSTALL
|
||||||
service = [] # per-request service time, to split the round trip
|
service = [] # per-request service time, to split the round trip
|
||||||
|
# ⚠️ A plain bytes/elapsed average is NOT the capability of the path: the
|
||||||
|
# game reads in bursts, so any window that contains idle time understates
|
||||||
|
# what the link can do. Tracking the gap between consecutive requests
|
||||||
|
# separates the two -- gaps under IDLE_GAP are the round trip (we are
|
||||||
|
# streaming), longer ones are the game not asking.
|
||||||
|
IDLE_GAP = 0.010
|
||||||
|
active_time = 0.0 # seconds actually spent streaming
|
||||||
|
active_bytes = 0
|
||||||
|
idle_time = 0.0
|
||||||
|
intervals = [] # inter-request gaps while active
|
||||||
|
t_prev = None
|
||||||
served = 0
|
served = 0
|
||||||
t_first = None
|
t_first = None
|
||||||
t_report = time.time()
|
t_report = time.time()
|
||||||
@@ -510,6 +521,15 @@ def main():
|
|||||||
reads += 1
|
reads += 1
|
||||||
now = time.time()
|
now = time.time()
|
||||||
service.append(now - t_req)
|
service.append(now - t_req)
|
||||||
|
if t_prev is not None:
|
||||||
|
gap = now - t_prev
|
||||||
|
if gap <= IDLE_GAP:
|
||||||
|
active_time += gap
|
||||||
|
active_bytes += len(data)
|
||||||
|
intervals.append(gap)
|
||||||
|
else:
|
||||||
|
idle_time += gap
|
||||||
|
t_prev = now
|
||||||
if t_first is None:
|
if t_first is None:
|
||||||
t_first = now
|
t_first = now
|
||||||
print(f"[{peer[0]}] GET_FILE {path!r} streaming started")
|
print(f"[{peer[0]}] GET_FILE {path!r} streaming started")
|
||||||
@@ -520,16 +540,26 @@ def main():
|
|||||||
el = max(now - t_first, 1e-9)
|
el = max(now - t_first, 1e-9)
|
||||||
svc = sorted(service)
|
svc = sorted(service)
|
||||||
med = svc[len(svc) // 2] * 1000 if svc else 0.0
|
med = svc[len(svc) // 2] * 1000 if svc else 0.0
|
||||||
rtt = (el / reads) * 1000 if reads else 0.0
|
iv = sorted(intervals)
|
||||||
print(f" streaming {served/el/1024:7.0f} KB/s "
|
rtt = iv[len(iv) // 2] * 1000 if iv else 0.0
|
||||||
f"{served/2**20:8.1f} MiB in {el:6.1f}s "
|
duty = 100 * active_time / el
|
||||||
f"{reads/el:5.0f} reads/s {len(data)} B/reply")
|
cap = (active_bytes / active_time / 1024) if active_time else 0
|
||||||
# One request in flight, so the observed interval IS the round
|
print(f" average {served/el/1024:7.0f} KB/s over {el:6.1f}s "
|
||||||
# trip. Splitting it shows whether WE are the slow part.
|
f"({served/2**20:7.1f} MiB, {len(data)} B/reply)")
|
||||||
print(f" round trip {rtt:5.2f} ms, of which this server "
|
# The capability figure: bytes served divided by the time
|
||||||
f"{med:5.2f} ms (median) -> "
|
# actually spent serving, with idle gaps excluded. This is
|
||||||
f"{'SERVER-bound' if med > rtt/2 else 'console-bound'}")
|
# what the path can do; the average above is that times the
|
||||||
|
# duty cycle, i.e. how much the game happened to ask for.
|
||||||
|
print(f" CAPABLE {cap:7.0f} KB/s while streaming "
|
||||||
|
f"duty {duty:5.1f}% idle {idle_time:5.1f}s")
|
||||||
|
# One request in flight, so the median gap IS the round trip.
|
||||||
|
print(f" round trip {rtt:5.2f} ms median, of which this "
|
||||||
|
f"server {med:5.2f} ms -> "
|
||||||
|
f"{'SERVER-bound' if med > rtt / 2 else 'console-bound'}"
|
||||||
|
f"; ceiling at this RTT = "
|
||||||
|
f"{(len(data)/(rtt/1000))/1024 if rtt else 0:.0f} KB/s")
|
||||||
service.clear()
|
service.clear()
|
||||||
|
intervals.clear()
|
||||||
t_report = now
|
t_report = now
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user