test(peer-cli): measure single-source download throughput
Add peer-cli accounting for download sessions so terminal download events report bytes, chunks, elapsed time, MiB/s, and Mbit/s. The extended scenario runner now has S37, a focused single-source download benchmark that creates a 2 GiB sparse bf1942 archive, downloads it from one peer with install disabled, and checks the destination archive size and reported byte count. This gives the QUIC performance work a repeatable measurement below the 5 GiB limit from the original request. The source file is sparse, so S37 is aimed at the app, QUIC, and destination-write path rather than raw source-disk reads; the existing correctness scenarios still cover normal game downloads. Baseline S37 before QUIC tuning: - 733.22 MiB/s - 6150.72 Mbit/s - 2.793s for 2.00 GiB plus version.ini - 65 reported chunks Test Plan: - just fmt - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S37 --build-image Refs: local LAN download performance investigation on 2026-05-20.
This commit is contained in:
@@ -26,6 +26,8 @@ CONTAINER_PREFIX = "lanspread-peer-cli-ext"
|
||||
CATALOG_DB = "/app/game.db"
|
||||
FIXTURES = REPO / "crates" / "lanspread-peer-cli" / "fixtures"
|
||||
CHUNK_SIZE = 32 * 1024 * 1024
|
||||
PERF_GAME_ID = "bf1942"
|
||||
PERF_GAME_SIZE = 2 * 1024 * 1024 * 1024
|
||||
IGNORED_DIFF_NAMES = {".lanspread", ".lanspread.json", "local"}
|
||||
|
||||
|
||||
@@ -322,6 +324,7 @@ class Runner:
|
||||
("S34", self.s34_many_small_files),
|
||||
("S35", self.s35_unknown_game_filtered),
|
||||
("S36", self.s36_latest_singleton),
|
||||
("S37", self.s37_single_source_download_throughput),
|
||||
]
|
||||
|
||||
for scenario_id, scenario in scenarios:
|
||||
@@ -1015,6 +1018,45 @@ class Runner:
|
||||
raise ScenarioError("got-game-files had no descriptors")
|
||||
return "client reported latest 20260501 with peer_count=5; only singleton latest peer sent chunks; diff matched"
|
||||
|
||||
def s37_single_source_download_throughput(self) -> str:
|
||||
source_dir = self.fixture_root / "s37-source"
|
||||
create_large_sparse_game(source_dir / PERF_GAME_ID, size=PERF_GAME_SIZE)
|
||||
source = self.peer("s37-source", games_dir=source_dir)
|
||||
client = self.peer("s37-client")
|
||||
connect_many(client, [source])
|
||||
wait_remote_game(client, PERF_GAME_ID, peer_count=1, version="20260520")
|
||||
|
||||
waiter = LineWaiter(len(client.output))
|
||||
client.send({"cmd": "download", "game_id": PERF_GAME_ID, "install": False})
|
||||
finished = client.wait_for(
|
||||
event_is("download-finished", PERF_GAME_ID),
|
||||
timeout=300,
|
||||
description=f"{PERF_GAME_ID} throughput download",
|
||||
waiter=waiter,
|
||||
)
|
||||
destination_archive = client.host_games_dir / PERF_GAME_ID / f"{PERF_GAME_ID}.eti"
|
||||
if destination_archive.stat().st_size != PERF_GAME_SIZE:
|
||||
raise ScenarioError(
|
||||
f"downloaded archive size mismatch: {destination_archive.stat().st_size} != {PERF_GAME_SIZE}"
|
||||
)
|
||||
|
||||
throughput = finished.get("data", {}).get("throughput")
|
||||
if not throughput:
|
||||
raise ScenarioError(f"download-finished did not include throughput: {finished}")
|
||||
expected_bytes = PERF_GAME_SIZE + len("20260520")
|
||||
if int(throughput["bytes"]) != expected_bytes:
|
||||
raise ScenarioError(
|
||||
f"throughput byte count mismatch: {throughput['bytes']} != {expected_bytes}"
|
||||
)
|
||||
|
||||
return (
|
||||
f"{PERF_GAME_ID} {format_bytes(PERF_GAME_SIZE)} single-source download: "
|
||||
f"{throughput['mib_per_s']:.2f} MiB/s, "
|
||||
f"{throughput['mbit_per_s']:.2f} Mbit/s, "
|
||||
f"{throughput['duration_ms'] / 1000.0:.3f}s, "
|
||||
f"{throughput['chunks']} chunks"
|
||||
)
|
||||
|
||||
|
||||
def run(command: list[str], description: str) -> subprocess.CompletedProcess[str]:
|
||||
result = subprocess.run(
|
||||
@@ -1122,6 +1164,20 @@ def create_many_small_game(root: Path) -> None:
|
||||
(root / "version.ini").write_text("20250101", encoding="utf-8")
|
||||
|
||||
|
||||
def create_large_sparse_game(root: Path, *, size: int) -> None:
|
||||
if root.exists():
|
||||
shutil.rmtree(root)
|
||||
root.mkdir(parents=True)
|
||||
(root / "version.ini").write_text("20260520", encoding="utf-8")
|
||||
archive = root / f"{root.name}.eti"
|
||||
with archive.open("wb") as handle:
|
||||
handle.truncate(size)
|
||||
|
||||
|
||||
def format_bytes(size: int) -> str:
|
||||
return f"{size / 1024 / 1024 / 1024:.2f} GiB"
|
||||
|
||||
|
||||
def connect_many(client: Peer, peers: list[Peer]) -> None:
|
||||
for peer in peers:
|
||||
client.connect_to(peer)
|
||||
|
||||
Reference in New Issue
Block a user