summaryrefslogtreecommitdiff
path: root/tests/udp_modes.py
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-28 17:11:33 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-28 17:11:33 +0100
commit9ec918187ab65488ebcce427f5a14a6b38e87b78 (patch)
tree4110ae8b03a67628cd020675159b64c765fb4f07 /tests/udp_modes.py
parentf3f640b6bcf10ba463fc81cec97cfa212c520cfe (diff)
Add load tests to sanitize check the trackerlogic, based on udp requests
Diffstat (limited to 'tests/udp_modes.py')
-rw-r--r--tests/udp_modes.py139
1 files changed, 139 insertions, 0 deletions
diff --git a/tests/udp_modes.py b/tests/udp_modes.py
new file mode 100644
index 0000000..19cd264
--- /dev/null
+++ b/tests/udp_modes.py
@@ -0,0 +1,139 @@
1"""The four udp arrangements, side by side.
2
3 cc -O2 -pthread -o /tmp/udp_modes tests/udp_modes.c
4 cc -O2 -pthread -o /tmp/udp_load tests/udp_load.c
5 python3 tests/udp_modes.py
6
7Two questions at once. Down the table: does a shared socket stop scaling as
8threads are added? Across it: does taking several datagrams per syscall make
9that better, or only cheaper?
10
11The receiver does no tracker work by default, so the figure is the kernel's
12receive path and nothing else. WORK_NS=1500 dilutes it back towards what the
13tracker actually costs per announce.
14
15The number reported is cpu microseconds the receiver spent per datagram
16answered, not throughput. Throughput here says more about the load generator
17than the receiver, because both run on the same machine and an echo costs so
18little that the generator runs out of cores first. Cpu per datagram does not
19care: it is what the receive path costs, whoever is the bottleneck.
20"""
21import os
22import signal
23import socket
24import subprocess
25import sys
26import time
27
28MODES = os.environ.get("MODES", "shared per-thread").split()
29BATCHES = [int(n) for n in os.environ.get("BATCHES", "1 8 32").split()]
30THREAD_COUNTS = [int(n) for n in os.environ.get("THREADS", "1 2 4 8").split()]
31LOAD_THREADS = int(os.environ.get("LOAD_THREADS", "4"))
32WINDOW = int(os.environ.get("WINDOW", "32"))
33SECONDS = float(os.environ.get("SECONDS", "3"))
34REPEATS = int(os.environ.get("REPEATS", "2"))
35WORK_NS = int(os.environ.get("WORK_NS", "0"))
36RECEIVER = os.environ.get("UDP_MODES", "/tmp/udp_modes")
37LOADER = os.environ.get("UDP_LOAD", "/tmp/udp_load")
38PORT_BASE = int(os.environ.get("PORT_BASE", "20100"))
39
40
41def cpu_seconds(pid):
42 try:
43 out = subprocess.run(["ps", "-o", "time=", "-p", str(pid)], capture_output=True, text=True).stdout.strip()
44 except OSError:
45 return None
46 if not out:
47 return None
48 total = 0.0
49 for part in out.replace("-", ":").split(":"):
50 total = total * 60 + float(part)
51 return total
52
53
54def one_run(port, threads, mode, batch):
55 receiver = subprocess.Popen(
56 [RECEIVER, str(port), str(threads), mode, str(batch), str(WORK_NS)],
57 stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
58 )
59 for _ in range(200):
60 if receiver.poll() is not None:
61 return None, None
62 probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
63 probe.settimeout(0.1)
64 try:
65 probe.sendto(b"\x00" * 16, ("127.0.0.1", port))
66 probe.recv(64)
67 probe.close()
68 break
69 except OSError:
70 probe.close()
71 time.sleep(0.02)
72 else:
73 receiver.send_signal(signal.SIGINT)
74 receiver.wait()
75 return None, None
76
77 try:
78 subprocess.run([LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), "1", "10000", str(WINDOW)],
79 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
80 spent, wall = cpu_seconds(receiver.pid), time.time()
81 out = subprocess.run(
82 [LOADER, "127.0.0.1", str(port), str(LOAD_THREADS), str(SECONDS), "10000", str(WINDOW)],
83 capture_output=True, text=True,
84 ).stdout.strip()
85 used = cpu_seconds(receiver.pid)
86 finally:
87 receiver.send_signal(signal.SIGINT)
88 try:
89 receiver.wait(timeout=5)
90 except subprocess.TimeoutExpired:
91 receiver.kill()
92 receiver.wait()
93
94 head = out.split()
95 if not head or not head[0].replace(".", "").isdigit():
96 return None, None
97 cores = (used - spent) / max(time.time() - wall, 1e-9) if (used is not None and spent is not None) else None
98 return float(head[0]), cores
99
100
101def main():
102 for binary in (RECEIVER, LOADER):
103 if not os.path.exists(binary):
104 raise SystemExit("missing %s; see the comment at the top of tests/udp_modes.c" % binary)
105
106 print(" %d load threads x %d in flight, %.0f s x %d runs, %d ns work per datagram"
107 % (LOAD_THREADS, WINDOW, SECONDS, REPEATS, WORK_NS))
108 print(" cpu microseconds per datagram, and the throughput it was measured at")
109 header = " %-14s %-8s" % ("sockets", "threads")
110 for batch in BATCHES:
111 header += "%20s" % ("%d per syscall" % batch)
112 print(header)
113
114 port = PORT_BASE
115 for mode in MODES:
116 for threads in THREAD_COUNTS:
117 row = " %-14s %-8d" % (mode, threads)
118 for batch in BATCHES:
119 results = []
120 for _ in range(REPEATS):
121 port += 1
122 rate, cores = one_run(port, threads, mode, batch)
123 if rate:
124 results.append((rate, cores))
125 if not results:
126 row += "%18s" % "-"
127 continue
128 results.sort()
129 rate, cores = results[len(results) // 2]
130 if cores:
131 row += "%9.2f us %6.0fk/s" % (cores * 1e6 / rate, rate / 1000.0)
132 else:
133 row += "%20s" % ("%.0fk/s" % (rate / 1000.0))
134 print(row)
135 sys.stdout.flush()
136
137
138if __name__ == "__main__":
139 main()