summaryrefslogtreecommitdiff
path: root/tests/udp_scaling.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/udp_scaling.py')
-rw-r--r--tests/udp_scaling.py159
1 files changed, 159 insertions, 0 deletions
diff --git a/tests/udp_scaling.py b/tests/udp_scaling.py
new file mode 100644
index 0000000..7691fcb
--- /dev/null
+++ b/tests/udp_scaling.py
@@ -0,0 +1,159 @@
1"""Does listen.udp.workers help or hurt, on this platform, with this tracker?
2
3Starts a tracker once per worker count and drives it with tests/udp_load.c,
4which speaks the real protocol, so what comes out is the tracker's own
5throughput rather than an echo server's.
6
7 cc -O2 -pthread -o /tmp/udp_load tests/udp_load.c
8 make opentracker
9 python3 tests/udp_scaling.py
10
11Worker count 0 means no dedicated workers: udp is served from the main event
12loop, which is what a default build does.
13
14LOAD_THREADS and WINDOW decide how much load is offered. Both matter. Too
15little and the tracker idles while the figure measures round trips; too much
16and the kernel drops datagrams, every lost reply costs the generator a
17timeout, and the figure collapses.
18
19Three columns say which side the number came from. "cores" is the tracker's
20cpu, "load" the generator's. Whichever is near its machine's core count is
21the one being measured. "lost" counts datagrams that went unanswered: once
22it leaves zero the throughput figure is timeouts, not service, and the run
23needs a smaller WINDOW.
24"""
25import os
26import resource
27import signal
28import socket
29import subprocess
30import sys
31import tempfile
32import time
33
34TRACKER = os.environ.get("OPENTRACKER", "./opentracker")
35LOADER = os.environ.get("UDP_LOAD", "/tmp/udp_load")
36WORKER_COUNTS = [int(n) for n in os.environ.get("WORKERS", "0 1 2 4 8").split()]
37LOAD_THREADS = int(os.environ.get("LOAD_THREADS", "4"))
38SECONDS = float(os.environ.get("SECONDS", "5"))
39TORRENTS = int(os.environ.get("TORRENTS", "10000"))
40WINDOW = int(os.environ.get("WINDOW", "32"))
41REPEATS = int(os.environ.get("REPEATS", "3"))
42PORT_BASE = int(os.environ.get("PORT_BASE", "19960"))
43
44
45def start(port, workers):
46 directory = tempfile.mkdtemp(prefix="ot-udpscale-")
47 config = "listen.tcp 127.0.0.1:%d\nlisten.udp 127.0.0.1:%d\n" % (port, port)
48 if workers:
49 config += "listen.udp.workers %d\n" % workers
50 with open(os.path.join(directory, "conf"), "w") as handle:
51 handle.write(config)
52 log = open(os.path.join(directory, "log"), "w+b")
53 tracker = subprocess.Popen([TRACKER, "-f", os.path.join(directory, "conf")], stdout=log, stderr=log)
54 for _ in range(300):
55 if tracker.poll() is not None:
56 log.seek(0)
57 raise SystemExit("tracker exited at once on port %d with %d workers:\n%s"
58 % (port, workers, log.read().decode("utf-8", "replace")))
59 try:
60 socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
61 return tracker, log
62 except OSError:
63 time.sleep(0.02)
64 tracker.kill()
65 raise SystemExit("tracker never listened on %d" % port)
66
67
68def cpu_seconds(pid):
69 """Cumulative cpu time of a process, the one spelling ps has everywhere."""
70 try:
71 out = subprocess.run(["ps", "-o", "time=", "-p", str(pid)], capture_output=True, text=True).stdout.strip()
72 except OSError:
73 return None
74 if not out:
75 return None
76 total, parts = 0.0, out.replace("-", ":").split(":")
77 for part in parts:
78 total = total * 60 + float(part)
79 return total
80
81
82def reaped_cpu_seconds():
83 """Cpu of every child already waited for. The generator is the only one
84 that starts and finishes inside a measured run."""
85 spent = resource.getrusage(resource.RUSAGE_CHILDREN)
86 return spent.ru_utime + spent.ru_stime
87
88
89def stop(tracker):
90 tracker.send_signal(signal.SIGINT)
91 try:
92 tracker.wait(timeout=10)
93 except subprocess.TimeoutExpired:
94 tracker.kill()
95 tracker.wait()
96
97
98def parse(line):
99 """replies/s and the count of announces that were never answered"""
100 words = line.split()
101 if not words or not words[0].replace(".", "").isdigit():
102 return None, None
103 lost = 0
104 if "lost," in words:
105 lost = int(words[words.index("lost,") - 1])
106 return float(words[0]), lost
107
108
109def main():
110 if not os.path.exists(LOADER):
111 raise SystemExit("build the generator first: cc -O2 -pthread -o %s tests/udp_load.c" % LOADER)
112
113 print(" tracker %s, %d load threads x %d in flight, %d torrents, %.0f s x %d runs"
114 % (TRACKER, LOAD_THREADS, WINDOW, TORRENTS, SECONDS, REPEATS))
115 print(" %-8s %12s %7s %7s %7s %10s %s"
116 % ("workers", "median/s", "spread", "cores", "load", "lost", "vs 0 workers"))
117 baseline = None
118 for index, workers in enumerate(WORKER_COUNTS):
119 results = []
120 for repeat in range(REPEATS):
121 port = PORT_BASE + index * 16 + repeat
122 tracker, log = start(port, workers)
123 try:
124 # Warm up until the torrent set exists and the peers are in
125 # place, so the measured run is renewals rather than insertions
126 subprocess.run([LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), "2", str(TORRENTS), str(WINDOW)],
127 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
128 spent, offered, wall = cpu_seconds(tracker.pid), reaped_cpu_seconds(), time.time()
129 result = subprocess.run(
130 [LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), str(SECONDS), str(TORRENTS), str(WINDOW)],
131 capture_output=True, text=True,
132 )
133 used, drove, elapsed = cpu_seconds(tracker.pid), reaped_cpu_seconds(), max(time.time() - wall, 1e-9)
134 finally:
135 stop(tracker)
136 log.close()
137 rate, lost = parse(result.stdout.strip())
138 if rate is None:
139 continue
140 cores = (used - spent) / elapsed if (used is not None and spent is not None) else None
141 results.append((rate, cores, (drove - offered) / elapsed, lost))
142
143 if not results:
144 print(" %-8s no result" % workers)
145 continue
146 results.sort()
147 rate, cores, load, lost = results[len(results) // 2]
148 if baseline is None:
149 baseline = rate
150 # A spread wider than the differences between rows means the rows are
151 # not telling you anything
152 spread = 100.0 * (results[-1][0] - results[0][0]) / rate
153 print(" %-8s %12.0f %6.0f%% %7s %7.2f %10d %.2fx"
154 % (workers, rate, spread, ("%.2f" % cores) if cores else "?", load, lost, rate / baseline))
155 sys.stdout.flush()
156
157
158if __name__ == "__main__":
159 main()