summaryrefslogtreecommitdiff
path: root/tests/udp_modes.c
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.c
parentf3f640b6bcf10ba463fc81cec97cfa212c520cfe (diff)
Add load tests to sanitize check the trackerlogic, based on udp requests
Diffstat (limited to 'tests/udp_modes.c')
-rw-r--r--tests/udp_modes.c240
1 files changed, 240 insertions, 0 deletions
diff --git a/tests/udp_modes.c b/tests/udp_modes.c
new file mode 100644
index 0000000..654cfd8
--- /dev/null
+++ b/tests/udp_modes.c
@@ -0,0 +1,240 @@
1/* A receiver with the udp arrangements we are choosing between, so they can
2 be compared without any of them being built into the tracker first.
3
4 It echoes what it is sent, so tests/udp_load.c drives it unchanged. There
5 is no tracker work here on purpose: with nothing else happening, whatever
6 the kernel's receive path costs is the whole figure, which is the clearest
7 view of it. Pass a work time to dilute that back towards realism.
8
9 cc -O2 -pthread -o udp_modes tests/udp_modes.c
10 ./udp_modes 6969 4 shared 1
11 ./udp_modes 6969 4 shared 32
12 ./udp_modes 6969 4 per-thread 1
13 ./udp_modes 6969 4 per-thread 32
14
15 Arguments: port, threads, shared|per-thread, datagrams per syscall,
16 and optionally nanoseconds of pretend work per datagram. */
17
18#define _GNU_SOURCE /* struct mmsghdr on glibc */
19
20#include <errno.h>
21#include <netinet/in.h>
22#include <pthread.h>
23#include <signal.h>
24#include <stdatomic.h>
25#include <stdio.h>
26#include <stdlib.h>
27#include <string.h>
28#include <sys/socket.h>
29#include <time.h>
30#include <unistd.h>
31
32#if defined(__linux__) || defined(__FreeBSD__)
33#define HAVE_MMSG 1
34#endif
35
36#define MAX_BATCH 256
37#define BUFFER_SIZE 2048
38
39static int g_port;
40static long g_threads;
41static long g_batch;
42static long g_work_ns;
43static int g_shared_socket = -1; /* -1 means one socket per thread */
44static _Atomic int g_running = 1;
45
46static void stop(int sig) {
47 (void)sig;
48 atomic_store(&g_running, 0);
49}
50
51/* Which option the platform offers for spreading datagrams over a group of
52 sockets bound to one address. They are not the same thing: FreeBSD's plain
53 SO_REUSEPORT permits the bind without spreading anything, and Darwin's
54 hands every datagram to whichever socket bound last. */
55static const char *share_the_port(int sock) {
56 int one = 1;
57
58 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
59#ifdef SO_REUSEPORT_LB
60 if (!setsockopt(sock, SOL_SOCKET, SO_REUSEPORT_LB, &one, sizeof(one)))
61 return "SO_REUSEPORT_LB";
62#endif
63#ifdef SO_REUSEPORT
64 if (!setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)))
65 return "SO_REUSEPORT";
66#endif
67 return "neither";
68}
69
70static int bind_one(int share, const char **option) {
71 struct sockaddr_in address;
72 int sock = socket(AF_INET, SOCK_DGRAM, 0);
73
74 if (sock < 0)
75 return -1;
76 *option = share ? share_the_port(sock) : "exclusive";
77
78 memset(&address, 0, sizeof(address));
79 address.sin_family = AF_INET;
80 address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
81 address.sin_port = htons((uint16_t)g_port);
82 if (bind(sock, (struct sockaddr *)&address, sizeof(address)) < 0) {
83 close(sock);
84 return -1;
85 }
86 return sock;
87}
88
89static void pretend_to_work(void) {
90 struct timespec started, now;
91 long waited = 0;
92
93 if (!g_work_ns)
94 return;
95 clock_gettime(CLOCK_MONOTONIC, &started);
96 while (waited < g_work_ns) {
97 clock_gettime(CLOCK_MONOTONIC, &now);
98 waited = (now.tv_sec - started.tv_sec) * 1000000000L + (now.tv_nsec - started.tv_nsec);
99 }
100}
101
102static void one_at_a_time(int sock) {
103 char buffer[BUFFER_SIZE];
104 struct sockaddr_in from;
105 socklen_t length;
106 ssize_t got;
107
108 while (atomic_load_explicit(&g_running, memory_order_relaxed)) {
109 length = sizeof(from);
110 got = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&from, &length);
111 if (got < 0) {
112 if (errno == EINTR || errno == EAGAIN)
113 continue;
114 return;
115 }
116 pretend_to_work();
117 sendto(sock, buffer, (size_t)got, 0, (struct sockaddr *)&from, length);
118 }
119}
120
121#ifdef HAVE_MMSG
122static void in_batches(int sock) {
123 static _Thread_local char buffers[MAX_BATCH][BUFFER_SIZE];
124 static _Thread_local struct sockaddr_in froms[MAX_BATCH];
125 static _Thread_local struct iovec iovecs[MAX_BATCH];
126 static _Thread_local struct mmsghdr messages[MAX_BATCH];
127 int got, i;
128
129 while (atomic_load_explicit(&g_running, memory_order_relaxed)) {
130 for (i = 0; i < g_batch; ++i) {
131 iovecs[i].iov_base = buffers[i];
132 iovecs[i].iov_len = BUFFER_SIZE;
133 memset(messages + i, 0, sizeof(messages[i]));
134 messages[i].msg_hdr.msg_name = froms + i;
135 messages[i].msg_hdr.msg_namelen = sizeof(froms[i]);
136 messages[i].msg_hdr.msg_iov = iovecs + i;
137 messages[i].msg_hdr.msg_iovlen = 1;
138 }
139
140 /* WAITFORONE so a quiet receiver behaves like the unbatched one rather
141 than holding the first datagram until the batch fills */
142 got = recvmmsg(sock, messages, (unsigned)g_batch, MSG_WAITFORONE, NULL);
143 if (got < 0) {
144 if (errno == EINTR || errno == EAGAIN)
145 continue;
146 return;
147 }
148
149 for (i = 0; i < got; ++i) {
150 pretend_to_work();
151 iovecs[i].iov_len = messages[i].msg_len;
152 messages[i].msg_hdr.msg_namelen = sizeof(froms[i]);
153 }
154 sendmmsg(sock, messages, (unsigned)got, 0);
155 }
156}
157#endif
158
159static void *worker(void *args) {
160 const char *option = "";
161 int sock = g_shared_socket;
162
163 (void)args;
164 if (sock < 0) {
165 sock = bind_one(1, &option);
166 if (sock < 0) {
167 fprintf(stderr, " a worker could not bind: %s\n", strerror(errno));
168 return NULL;
169 }
170 }
171
172#ifdef HAVE_MMSG
173 if (g_batch > 1)
174 in_batches(sock);
175 else
176#endif
177 one_at_a_time(sock);
178
179 if (g_shared_socket < 0)
180 close(sock);
181 return NULL;
182}
183
184int main(int argc, char **argv) {
185 const char *how = argc > 3 ? argv[3] : "shared";
186 const char *option = "";
187 pthread_t *threads;
188 long i;
189
190 g_port = argc > 1 ? atoi(argv[1]) : 6969;
191 g_threads = argc > 2 ? atol(argv[2]) : 1;
192 g_batch = argc > 4 ? atol(argv[4]) : 1;
193 g_work_ns = argc > 5 ? atol(argv[5]) : 0;
194 if (g_batch < 1)
195 g_batch = 1;
196 if (g_batch > MAX_BATCH)
197 g_batch = MAX_BATCH;
198
199#ifndef HAVE_MMSG
200 if (g_batch > 1) {
201 fprintf(stderr, " no recvmmsg on this platform; batching unavailable\n");
202 return 2;
203 }
204#endif
205
206 signal(SIGINT, stop);
207 signal(SIGTERM, stop);
208
209 if (!strcmp(how, "shared")) {
210 g_shared_socket = bind_one(0, &option);
211 if (g_shared_socket < 0) {
212 fprintf(stderr, " bind failed: %s\n", strerror(errno));
213 return 1;
214 }
215 } else {
216 /* Probe once so the mode line can say what the platform gave us */
217 int probe = bind_one(1, &option);
218 if (probe < 0) {
219 fprintf(stderr, " bind failed: %s\n", strerror(errno));
220 return 1;
221 }
222 close(probe);
223 }
224
225 printf(" port %d, %ld threads, %s socket%s (%s), %ld per syscall, %ld ns work\n",
226 g_port, g_threads, strcmp(how, "shared") ? "one per thread" : "one shared",
227 strcmp(how, "shared") ? "s" : "", option, g_batch, g_work_ns);
228 fflush(stdout);
229
230 threads = calloc((size_t)g_threads, sizeof(pthread_t));
231 for (i = 0; i < g_threads; ++i)
232 pthread_create(threads + i, NULL, worker, NULL);
233 for (i = 0; i < g_threads; ++i)
234 pthread_join(threads[i], NULL);
235
236 if (g_shared_socket >= 0)
237 close(g_shared_socket);
238 free(threads);
239 return 0;
240}