summaryrefslogtreecommitdiff
path: root/tests/http_load.c
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-30 02:35:47 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-30 02:35:47 +0100
commitcc684e79c38e0aa5ff44820a31658f457691d979 (patch)
tree2c1b0829944751575703463d7699a25be67ebca6 /tests/http_load.c
parent95fd9a4ae2717a73bc446b29a87ad5bc9f00317e (diff)
Add an http load generator
Diffstat (limited to 'tests/http_load.c')
-rw-r--r--tests/http_load.c148
1 files changed, 148 insertions, 0 deletions
diff --git a/tests/http_load.c b/tests/http_load.c
new file mode 100644
index 0000000..d04139b
--- /dev/null
+++ b/tests/http_load.c
@@ -0,0 +1,148 @@
1/* An HTTP load generator speaking the tracker's own protocol.
2
3 Each request is its own connection, which is what a BitTorrent client
4 normally does and what the tracker's hot path is built for: accept, read
5 one request, write a short reply, close. That makes this a measurement of
6 connection cost rather than of request cost, and connection cost is where
7 the tracker's system calls are.
8
9 cc -O2 -pthread -o http_load tests/http_load.c
10 ./http_load 127.0.0.1 6969 8 5 10000
11
12 Arguments: host, port, threads, seconds, and how many distinct torrents to
13 announce. Cycling a fixed set reaches a steady state where almost every
14 announce renews a peer that is already there, rather than measuring how
15 fast the tracker can insert torrents it will never see again. */
16
17#include <arpa/inet.h>
18#include <errno.h>
19#include <netinet/in.h>
20#include <netinet/tcp.h>
21#include <pthread.h>
22#include <stdatomic.h>
23#include <stdint.h>
24#include <stdio.h>
25#include <stdlib.h>
26#include <string.h>
27#include <sys/socket.h>
28#include <sys/time.h>
29#include <unistd.h>
30
31static struct sockaddr_in g_tracker;
32static long g_torrents = 10000;
33static double g_seconds = 5;
34static _Atomic long long g_replies = 0;
35static _Atomic long long g_failed = 0;
36static _Atomic int g_running = 1;
37
38static double now_seconds(void) {
39 struct timeval tv;
40 gettimeofday(&tv, NULL);
41 return tv.tv_sec + tv.tv_usec / 1000000.0;
42}
43
44/* %xx for all twenty bytes, which is what a client sends and what the
45 tracker's urlencode reader has to walk */
46static void hash_of(long which, char *out) {
47 int i;
48
49 for (i = 0; i < 20; ++i) {
50 unsigned byte = (unsigned)((which >> ((i % 4) * 8)) & 0xff) ^ (unsigned)(i * 37);
51 sprintf(out + i * 3, "%%%02x", byte & 0xff);
52 }
53}
54
55static void *worker(void *args) {
56 long index = (long)(intptr_t)args;
57 long which = index * 7919;
58 char request[512], hash[61], reply[2048];
59
60 while (atomic_load_explicit(&g_running, memory_order_relaxed)) {
61 int sock = socket(AF_INET, SOCK_STREAM, 0);
62 int one = 1;
63 ssize_t got, sent;
64 size_t length;
65
66 if (sock < 0) {
67 atomic_fetch_add_explicit(&g_failed, 1, memory_order_relaxed);
68 continue;
69 }
70 setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
71
72 if (connect(sock, (struct sockaddr *)&g_tracker, sizeof(g_tracker)) < 0) {
73 atomic_fetch_add_explicit(&g_failed, 1, memory_order_relaxed);
74 close(sock);
75 continue;
76 }
77
78 hash_of(which, hash);
79 which = (which + 1) % g_torrents;
80 length = (size_t)snprintf(request, sizeof(request),
81 "GET /announce?info_hash=%s&peer_id=-OT0001-%06ld%06ld&port=%u&left=0 HTTP/1.0\r\n\r\n",
82 hash, index, which, (unsigned)(6881 + index));
83
84 for (sent = 0; (size_t)sent < length;) {
85 ssize_t wrote = write(sock, request + sent, length - (size_t)sent);
86 if (wrote <= 0)
87 break;
88 sent += wrote;
89 }
90
91 /* Read to end of reply, which the tracker marks by closing */
92 got = 0;
93 for (;;) {
94 ssize_t piece = read(sock, reply, sizeof(reply));
95 if (piece <= 0)
96 break;
97 got += piece;
98 }
99 close(sock);
100
101 if (got > 0)
102 atomic_fetch_add_explicit(&g_replies, 1, memory_order_relaxed);
103 else
104 atomic_fetch_add_explicit(&g_failed, 1, memory_order_relaxed);
105 }
106 return NULL;
107}
108
109int main(int argc, char **argv) {
110 const char *host = argc > 1 ? argv[1] : "127.0.0.1";
111 int port = argc > 2 ? atoi(argv[2]) : 6969;
112 long threads = argc > 3 ? atol(argv[3]) : 4;
113 pthread_t *pool;
114 double started, elapsed;
115 long i;
116
117 g_seconds = argc > 4 ? atof(argv[4]) : 5;
118 g_torrents = argc > 5 ? atol(argv[5]) : 10000;
119 if (g_torrents < 1)
120 g_torrents = 1;
121
122 memset(&g_tracker, 0, sizeof(g_tracker));
123 g_tracker.sin_family = AF_INET;
124 g_tracker.sin_port = htons((uint16_t)port);
125 if (inet_pton(AF_INET, host, &g_tracker.sin_addr) != 1) {
126 fprintf(stderr, "cannot parse address %s\n", host);
127 return 1;
128 }
129
130 pool = calloc((size_t)threads, sizeof(pthread_t));
131 started = now_seconds();
132 for (i = 0; i < threads; ++i)
133 pthread_create(pool + i, NULL, worker, (void *)(intptr_t)i);
134
135 while (now_seconds() - started < g_seconds)
136 usleep(20000);
137 atomic_store(&g_running, 0);
138
139 for (i = 0; i < threads; ++i)
140 pthread_join(pool[i], NULL);
141 elapsed = now_seconds() - started;
142
143 printf("%.0f replies/s (%lld replies, %lld failed, %ld threads, %.1f s)\n",
144 (double)atomic_load(&g_replies) / elapsed, (long long)atomic_load(&g_replies),
145 (long long)atomic_load(&g_failed), threads, elapsed);
146 free(pool);
147 return 0;
148}