summaryrefslogtreecommitdiff
path: root/tests/udp_load.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/udp_load.c')
-rw-r--r--tests/udp_load.c202
1 files changed, 202 insertions, 0 deletions
diff --git a/tests/udp_load.c b/tests/udp_load.c
new file mode 100644
index 0000000..d86c157
--- /dev/null
+++ b/tests/udp_load.c
@@ -0,0 +1,202 @@
1/* A UDP load generator speaking the tracker's own protocol.
2
3 Each thread keeps a socket of its own, does the BEP 15 connect handshake
4 once and then announces in a loop, counting replies. Separate sockets
5 matter: the kernel spreads datagrams across a SO_REUSEPORT group by a hash
6 of the four tuple, so a generator sending everything from one source port
7 would pile every packet onto one of the tracker's workers and measure the
8 wrong thing.
9
10 cc -O2 -pthread -o udp_load tests/udp_load.c
11 ./udp_load 127.0.0.1 6969 4 5 10000 64
12
13 The last argument is how many announces each thread keeps in flight. With
14 one, a thread sends and waits, so the offered load is threads divided by
15 the round trip and the figure says more about scheduling than about the
16 tracker: measured here, a tracker answering 60k/s that way was using
17 three quarters of one core. Raising the window offers load without
18 needing a thread per outstanding request, which is what lets the tracker
19 be pushed to the point where it is actually the limit.
20
21 The last argument is how many distinct torrents to announce. It is what
22 keeps the figure honest: a fresh info hash every time measures how fast
23 the tracker can insert a million torrents into sorted vectors, which is
24 not what a tracker spends its life doing, and which punishes whichever
25 configuration is quickest because it reaches the expensive end of the run
26 soonest. Cycling a fixed set reaches a steady state where almost every
27 announce renews a peer that is already there. */
28
29#include <arpa/inet.h>
30#include <netinet/in.h>
31#include <pthread.h>
32#include <stdatomic.h>
33#include <stdint.h>
34#include <stdio.h>
35#include <stdlib.h>
36#include <string.h>
37#include <sys/socket.h>
38#include <sys/time.h>
39#include <time.h>
40#include <unistd.h>
41
42#define CONNECT_MAGIC 0x41727101980ULL
43
44static struct sockaddr_in g_tracker;
45static long g_torrents = 10000;
46static long g_window = 64;
47static _Atomic long long g_sent = 0;
48static _Atomic int g_running = 1;
49static _Atomic long long g_replies = 0;
50static _Atomic long long g_timeouts = 0;
51
52static double seconds(void) {
53 struct timespec now;
54 clock_gettime(CLOCK_MONOTONIC, &now);
55 return now.tv_sec + now.tv_nsec / 1e9;
56}
57
58static void put64(unsigned char *at, uint64_t value) {
59 int i;
60 for (i = 0; i < 8; ++i)
61 at[i] = (unsigned char)(value >> (56 - 8 * i));
62}
63static void put32(unsigned char *at, uint32_t value) {
64 at[0] = (unsigned char)(value >> 24);
65 at[1] = (unsigned char)(value >> 16);
66 at[2] = (unsigned char)(value >> 8);
67 at[3] = (unsigned char)value;
68}
69static uint64_t get64(const unsigned char *at) {
70 uint64_t value = 0;
71 int i;
72 for (i = 0; i < 8; ++i)
73 value = (value << 8) | at[i];
74 return value;
75}
76
77static void *loader(void *args) {
78 long index = (long)args;
79 unsigned char request[98], reply[1500];
80 uint64_t connection_id = 0;
81 int sock = socket(AF_INET, SOCK_DGRAM, 0);
82 struct timeval patience = {0, 50000}; /* 50 ms */
83 unsigned seed = (unsigned)index * 2654435761u + 1;
84
85 if (sock < 0)
86 return NULL;
87 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &patience, sizeof(patience));
88
89 /* connect handshake */
90 memset(request, 0, sizeof(request));
91 put64(request, CONNECT_MAGIC);
92 put32(request + 8, 0);
93 put32(request + 12, (uint32_t)index);
94 if (sendto(sock, request, 16, 0, (struct sockaddr *)&g_tracker, sizeof(g_tracker)) != 16) {
95 close(sock);
96 return NULL;
97 }
98 if (recvfrom(sock, reply, sizeof(reply), 0, NULL, NULL) < 16) {
99 fprintf(stderr, " thread %ld: no answer to connect\n", index);
100 close(sock);
101 return NULL;
102 }
103 connection_id = get64(reply + 8);
104
105 /* Keep g_window announces outstanding: send that many, then drain what has
106 come back, rather than waiting for each reply in turn */
107 while (atomic_load_explicit(&g_running, memory_order_relaxed)) {
108 ssize_t got;
109 int i;
110 long outstanding;
111
112 for (outstanding = 0; outstanding < g_window; ++outstanding) {
113 memset(request, 0, sizeof(request));
114 put64(request, connection_id);
115 put32(request + 8, 1); /* announce */
116 put32(request + 12, (uint32_t)index);
117 /* One of a fixed set, so that after the first pass almost every announce
118 renews a peer rather than creating a torrent. The index is hashed into
119 all twenty bytes so the buckets, which key off the first four, are
120 spread rather than walked in order. */
121 {
122 unsigned which = (unsigned)((seed = seed * 1103515245u + 12345u) >> 8) % (unsigned)g_torrents;
123 unsigned spread;
124 for (i = 0; i < 20; ++i) {
125 spread = which * 2654435761u + (unsigned)i * 40503u;
126 spread ^= spread >> 13;
127 request[16 + i] = (unsigned char)(spread >> 11);
128 }
129 }
130 for (i = 0; i < 20; ++i)
131 request[36 + i] = (unsigned char)('a' + (i % 26));
132 put64(request + 56, 0); /* downloaded */
133 put64(request + 64, 0); /* left: a seeder */
134 put64(request + 72, 0); /* uploaded */
135 put32(request + 80, 0); /* event */
136 put32(request + 84, 0); /* ip */
137 put32(request + 88, (uint32_t)index); /* key */
138 put32(request + 92, (uint32_t)-1); /* num_want */
139 /* A port of this thread's own, so each thread is a distinct peer and the
140 swarms hold more than one */
141 request[96] = (unsigned char)((6881 + index) >> 8);
142 request[97] = (unsigned char)((6881 + index) & 0xff);
143
144 if (sendto(sock, request, 98, 0, (struct sockaddr *)&g_tracker, sizeof(g_tracker)) != 98)
145 break;
146 atomic_fetch_add_explicit(&g_sent, 1, memory_order_relaxed);
147 }
148
149 for (outstanding = 0; outstanding < g_window; ++outstanding) {
150 got = recvfrom(sock, reply, sizeof(reply), 0, NULL, NULL);
151 if (got >= 20)
152 atomic_fetch_add_explicit(&g_replies, 1, memory_order_relaxed);
153 else {
154 atomic_fetch_add_explicit(&g_timeouts, 1, memory_order_relaxed);
155 break; /* nothing more is coming; go round and send again */
156 }
157 }
158 }
159
160 close(sock);
161 return NULL;
162}
163
164int main(int argc, char **argv) {
165 const char *host = argc > 1 ? argv[1] : "127.0.0.1";
166 int port = argc > 2 ? atoi(argv[2]) : 6969;
167 long threads = argc > 3 ? atol(argv[3]) : 16;
168 double run_for = argc > 4 ? atof(argv[4]) : 5.0;
169 long torrents = argc > 5 ? atol(argv[5]) : 10000;
170 long window = argc > 6 ? atol(argv[6]) : 64;
171 pthread_t *ids = calloc((size_t)threads, sizeof(pthread_t));
172 long i;
173 double started, elapsed;
174
175 g_torrents = torrents > 0 ? torrents : 1;
176 g_window = window > 0 ? window : 1;
177
178 memset(&g_tracker, 0, sizeof(g_tracker));
179 g_tracker.sin_family = AF_INET;
180 g_tracker.sin_port = htons((uint16_t)port);
181 if (inet_pton(AF_INET, host, &g_tracker.sin_addr) != 1) {
182 fprintf(stderr, "cannot parse address %s\n", host);
183 return 1;
184 }
185
186 for (i = 0; i < threads; ++i)
187 pthread_create(ids + i, NULL, loader, (void *)i);
188
189 started = seconds();
190 usleep((useconds_t)(run_for * 1e6));
191 atomic_store(&g_running, 0);
192 elapsed = seconds() - started;
193
194 for (i = 0; i < threads; ++i)
195 pthread_join(ids[i], NULL);
196
197 printf("%.0f replies/s (%.0f sent/s, %lld replies, %lld lost, %ld threads x %ld in flight, %.1f s)\n",
198 (double)atomic_load(&g_replies) / elapsed, (double)atomic_load(&g_sent) / elapsed,
199 (long long)atomic_load(&g_replies), (long long)atomic_load(&g_timeouts), threads, g_window, elapsed);
200 free(ids);
201 return 0;
202}