benchmark: fix rsync network throttling via rsync daemon
CI / build-and-test (push) Successful in 48s
CI / build-and-test (pull_request) Successful in 47s

rsync local-to-local copies bypass the network stack entirely,
making tc/netem ineffective. Start an rsync daemon on TCP so
both tools see the same network conditions.
This commit is contained in:
2026-07-18 20:08:54 +02:00
parent f0c7bb791a
commit 4ed514ad1b
+68 -4
View File
@@ -21,6 +21,7 @@ import socket
import statistics import statistics
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
@@ -55,6 +56,59 @@ RSYNC_CONFIGS = [
{"name": "rsync -z --zstd", "flags": ["-z", "--zc", "zstd"],"tool": "rsync"}, {"name": "rsync -z --zstd", "flags": ["-z", "--zc", "zstd"],"tool": "rsync"},
] ]
class RsyncDaemon:
"""Manages an rsync daemon for network-fair benchmarking."""
def __init__(self):
self._proc = None
self._port = None
self._conf_dir = None
self._module_path = None
def start(self, source_dir):
self._port = find_free_port()
self._conf_dir = tempfile.mkdtemp(prefix="rsyncd_")
self._module_path = source_dir
conf_path = os.path.join(self._conf_dir, "rsyncd.conf")
log_path = os.path.join(self._conf_dir, "rsyncd.log")
with open(conf_path, "w") as f:
f.write(f"uid = 0\ngid = 0\nuse chroot = no\nlog file = {log_path}\n")
f.write(f"[bench]\n\tpath = {source_dir}\n\tread only = yes\n")
self._proc = subprocess.Popen(
["rsync", "--daemon", "--no-detach",
"--port", str(self._port),
"--config", conf_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
wait_for_port(self._port, timeout=5)
def stop(self):
if self._proc:
self._proc.terminate()
try:
self._proc.wait(timeout=3)
except subprocess.TimeoutExpired:
self._proc.kill()
self._proc.wait()
self._proc = None
if self._conf_dir:
shutil.rmtree(self._conf_dir, ignore_errors=True)
self._conf_dir = None
@property
def source_url(self):
return f"rsync://127.0.0.1:{self._port}/bench/"
def __enter__(self):
return self
def __exit__(self, *args):
self.stop()
STRUCTURED_FILES = { STRUCTURED_FILES = {
"small.txt": b"hello world\n", "small.txt": b"hello world\n",
"medium.txt": b"the quick brown fox jumps over the lazy dog\n" * 5000, "medium.txt": b"the quick brown fox jumps over the lazy dog\n" * 5000,
@@ -170,9 +224,11 @@ def run_fastsync(source_dir, dest_dir, flags, port):
return None return None
def run_rsync(source_dir, dest_dir, flags): def run_rsync(source_dir, dest_dir, flags, rsync_daemon=None):
"""Run rsync. Returns duration or None.""" """Run rsync. Returns duration or None."""
src = source_dir.rstrip("/") + "/" src = source_dir.rstrip("/") + "/"
if rsync_daemon:
src = rsync_daemon.source_url
cmd = ["rsync", "-a", "--delete"] + flags + [src, dest_dir + "/"] cmd = ["rsync", "-a", "--delete"] + flags + [src, dest_dir + "/"]
try: try:
start = time.monotonic() start = time.monotonic()
@@ -185,10 +241,10 @@ def run_rsync(source_dir, dest_dir, flags):
return None return None
def run_transfer(config, source_dir, dest_dir, port=None): def run_transfer(config, source_dir, dest_dir, port=None, rsync_daemon=None):
"""Route to the right tool. Returns duration or None.""" """Route to the right tool. Returns duration or None."""
if config["tool"] == "rsync": if config["tool"] == "rsync":
return run_rsync(source_dir, dest_dir, config["flags"]) return run_rsync(source_dir, dest_dir, config["flags"], rsync_daemon)
else: else:
return run_fastsync(source_dir, dest_dir, config["flags"], port) return run_fastsync(source_dir, dest_dir, config["flags"], port)
@@ -196,10 +252,16 @@ def run_transfer(config, source_dir, dest_dir, port=None):
def run_benchmark(source_dir, dest_dir, configs, runs, profile_name): def run_benchmark(source_dir, dest_dir, configs, runs, profile_name):
"""Run benchmark for all configs, returns list of results.""" """Run benchmark for all configs, returns list of results."""
is_limited = profile_name != "unlimited" is_limited = profile_name != "unlimited"
has_rsync = any(c["tool"] == "rsync" for c in configs)
if is_limited: if is_limited:
netem_apply_profile(profile_name) netem_apply_profile(profile_name)
rsync_daemon = None
try: try:
if is_limited and has_rsync:
rsync_daemon = RsyncDaemon()
rsync_daemon.start(source_dir)
results = [] results = []
for config in configs: for config in configs:
times = [] times = []
@@ -218,7 +280,7 @@ def run_benchmark(source_dir, dest_dir, configs, runs, profile_name):
) )
wait_for_port(port) wait_for_port(port)
t = run_transfer(config, source_dir, dest_dir, port) t = run_transfer(config, source_dir, dest_dir, port, rsync_daemon)
if t is not None: if t is not None:
times.append(t) times.append(t)
finally: finally:
@@ -241,6 +303,8 @@ def run_benchmark(source_dir, dest_dir, configs, runs, profile_name):
results.append(entry) results.append(entry)
return results return results
finally: finally:
if rsync_daemon:
rsync_daemon.stop()
if is_limited: if is_limited:
netem_reset() netem_reset()