From 82faf0af92e6c3a8c56d591e674b33708f60aa32 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Wed, 28 Oct 2026 17:52:16 +0100 Subject: Let the load tester log losses to better gauge performance --- tests/udp_scaling.py | 64 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 20 deletions(-) (limited to 'tests/udp_scaling.py') diff --git a/tests/udp_scaling.py b/tests/udp_scaling.py index b5048cd..7691fcb 100644 --- a/tests/udp_scaling.py +++ b/tests/udp_scaling.py @@ -14,10 +14,16 @@ loop, which is what a default build does. LOAD_THREADS and WINDOW decide how much load is offered. Both matter. Too little and the tracker idles while the figure measures round trips; too much and the kernel drops datagrams, every lost reply costs the generator a -timeout, and the figure collapses. The generator prints how many were lost: -tune until that is near zero and the throughput has stopped rising. +timeout, and the figure collapses. + +Three columns say which side the number came from. "cores" is the tracker's +cpu, "load" the generator's. Whichever is near its machine's core count is +the one being measured. "lost" counts datagrams that went unanswered: once +it leaves zero the throughput figure is timeouts, not service, and the run +needs a smaller WINDOW. """ import os +import resource import signal import socket import subprocess @@ -73,6 +79,13 @@ def cpu_seconds(pid): return total +def reaped_cpu_seconds(): + """Cpu of every child already waited for. The generator is the only one + that starts and finishes inside a measured run.""" + spent = resource.getrusage(resource.RUSAGE_CHILDREN) + return spent.ru_utime + spent.ru_stime + + def stop(tracker): tracker.send_signal(signal.SIGINT) try: @@ -82,17 +95,28 @@ def stop(tracker): tracker.wait() +def parse(line): + """replies/s and the count of announces that were never answered""" + words = line.split() + if not words or not words[0].replace(".", "").isdigit(): + return None, None + lost = 0 + if "lost," in words: + lost = int(words[words.index("lost,") - 1]) + return float(words[0]), lost + + def main(): if not os.path.exists(LOADER): raise SystemExit("build the generator first: cc -O2 -pthread -o %s tests/udp_load.c" % LOADER) print(" tracker %s, %d load threads x %d in flight, %d torrents, %.0f s x %d runs" % (TRACKER, LOAD_THREADS, WINDOW, TORRENTS, SECONDS, REPEATS)) - print(" %-8s %12s %12s %12s %7s %s" - % ("workers", "median/s", "lowest", "highest", "cores", "vs 0 workers")) + print(" %-8s %12s %7s %7s %7s %10s %s" + % ("workers", "median/s", "spread", "cores", "load", "lost", "vs 0 workers")) baseline = None for index, workers in enumerate(WORKER_COUNTS): - rates, cores = [], [] + results = [] for repeat in range(REPEATS): port = PORT_BASE + index * 16 + repeat tracker, log = start(port, workers) @@ -101,34 +125,34 @@ def main(): # place, so the measured run is renewals rather than insertions subprocess.run([LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), "2", str(TORRENTS), str(WINDOW)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - spent, wall = cpu_seconds(tracker.pid), time.time() + spent, offered, wall = cpu_seconds(tracker.pid), reaped_cpu_seconds(), time.time() result = subprocess.run( [LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), str(SECONDS), str(TORRENTS), str(WINDOW)], capture_output=True, text=True, ) - used = cpu_seconds(tracker.pid) + used, drove, elapsed = cpu_seconds(tracker.pid), reaped_cpu_seconds(), max(time.time() - wall, 1e-9) finally: stop(tracker) log.close() - head = result.stdout.strip().split() - if head and head[0].replace(".", "").isdigit(): - rates.append(float(head[0])) - if used is not None and spent is not None: - cores.append((used - spent) / max(time.time() - wall, 1e-9)) + rate, lost = parse(result.stdout.strip()) + if rate is None: + continue + cores = (used - spent) / elapsed if (used is not None and spent is not None) else None + results.append((rate, cores, (drove - offered) / elapsed, lost)) - if not rates: + if not results: print(" %-8s no result" % workers) continue - rates.sort() - median = rates[len(rates) // 2] + results.sort() + rate, cores, load, lost = results[len(results) // 2] if baseline is None: - baseline = median + baseline = rate # A spread wider than the differences between rows means the rows are # not telling you anything - busy = ("%7.2f" % (sorted(cores)[len(cores) // 2])) if cores else " ?" - print(" %-8s %12.0f %12.0f %12.0f %s %.2fx (spread %.0f%%)" - % (workers, median, rates[0], rates[-1], busy, median / baseline, - 100.0 * (rates[-1] - rates[0]) / median)) + spread = 100.0 * (results[-1][0] - results[0][0]) / rate + print(" %-8s %12.0f %6.0f%% %7s %7.2f %10d %.2fx" + % (workers, rate, spread, ("%.2f" % cores) if cores else "?", load, lost, rate / baseline)) + sys.stdout.flush() if __name__ == "__main__": -- cgit v1.3