diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-28 17:11:33 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-28 17:11:33 +0100 |
| commit | 9ec918187ab65488ebcce427f5a14a6b38e87b78 (patch) | |
| tree | 4110ae8b03a67628cd020675159b64c765fb4f07 /tests/udp_load.c | |
| parent | f3f640b6bcf10ba463fc81cec97cfa212c520cfe (diff) | |
Add load tests to sanitize check the trackerlogic, based on udp requests
Diffstat (limited to 'tests/udp_load.c')
| -rw-r--r-- | tests/udp_load.c | 202 |
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 | |||
| 44 | static struct sockaddr_in g_tracker; | ||
| 45 | static long g_torrents = 10000; | ||
| 46 | static long g_window = 64; | ||
| 47 | static _Atomic long long g_sent = 0; | ||
| 48 | static _Atomic int g_running = 1; | ||
| 49 | static _Atomic long long g_replies = 0; | ||
| 50 | static _Atomic long long g_timeouts = 0; | ||
| 51 | |||
| 52 | static double seconds(void) { | ||
| 53 | struct timespec now; | ||
| 54 | clock_gettime(CLOCK_MONOTONIC, &now); | ||
| 55 | return now.tv_sec + now.tv_nsec / 1e9; | ||
| 56 | } | ||
| 57 | |||
| 58 | static 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 | } | ||
| 63 | static 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 | } | ||
| 69 | static 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 | |||
| 77 | static 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 | |||
| 164 | int 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 | } | ||
