summaryrefslogtreecommitdiff
path: root/tests/udp_load.c
blob: d86c157810a99f0d147d059aa3c8e19429a617c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/* A UDP load generator speaking the tracker's own protocol.

   Each thread keeps a socket of its own, does the BEP 15 connect handshake
   once and then announces in a loop, counting replies. Separate sockets
   matter: the kernel spreads datagrams across a SO_REUSEPORT group by a hash
   of the four tuple, so a generator sending everything from one source port
   would pile every packet onto one of the tracker's workers and measure the
   wrong thing.

     cc -O2 -pthread -o udp_load tests/udp_load.c
     ./udp_load 127.0.0.1 6969 4 5 10000 64

   The last argument is how many announces each thread keeps in flight. With
   one, a thread sends and waits, so the offered load is threads divided by
   the round trip and the figure says more about scheduling than about the
   tracker: measured here, a tracker answering 60k/s that way was using
   three quarters of one core. Raising the window offers load without
   needing a thread per outstanding request, which is what lets the tracker
   be pushed to the point where it is actually the limit.

   The last argument is how many distinct torrents to announce. It is what
   keeps the figure honest: a fresh info hash every time measures how fast
   the tracker can insert a million torrents into sorted vectors, which is
   not what a tracker spends its life doing, and which punishes whichever
   configuration is quickest because it reaches the expensive end of the run
   soonest. Cycling a fixed set reaches a steady state where almost every
   announce renews a peer that is already there. */

#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>

#define CONNECT_MAGIC 0x41727101980ULL

static struct sockaddr_in g_tracker;
static long               g_torrents = 10000;
static long               g_window   = 64;
static _Atomic long long  g_sent     = 0;
static _Atomic int        g_running  = 1;
static _Atomic long long  g_replies  = 0;
static _Atomic long long  g_timeouts = 0;

static double             seconds(void) {
  struct timespec now;
  clock_gettime(CLOCK_MONOTONIC, &now);
  return now.tv_sec + now.tv_nsec / 1e9;
}

static void put64(unsigned char *at, uint64_t value) {
  int i;
  for (i = 0; i < 8; ++i)
    at[i] = (unsigned char)(value >> (56 - 8 * i));
}
static void put32(unsigned char *at, uint32_t value) {
  at[0] = (unsigned char)(value >> 24);
  at[1] = (unsigned char)(value >> 16);
  at[2] = (unsigned char)(value >> 8);
  at[3] = (unsigned char)value;
}
static uint64_t get64(const unsigned char *at) {
  uint64_t value = 0;
  int      i;
  for (i = 0; i < 8; ++i)
    value = (value << 8) | at[i];
  return value;
}

static void *loader(void *args) {
  long          index = (long)args;
  unsigned char request[98], reply[1500];
  uint64_t      connection_id = 0;
  int           sock          = socket(AF_INET, SOCK_DGRAM, 0);
  struct timeval patience     = {0, 50000}; /* 50 ms */
  unsigned      seed          = (unsigned)index * 2654435761u + 1;

  if (sock < 0)
    return NULL;
  setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &patience, sizeof(patience));

  /* connect handshake */
  memset(request, 0, sizeof(request));
  put64(request, CONNECT_MAGIC);
  put32(request + 8, 0);
  put32(request + 12, (uint32_t)index);
  if (sendto(sock, request, 16, 0, (struct sockaddr *)&g_tracker, sizeof(g_tracker)) != 16) {
    close(sock);
    return NULL;
  }
  if (recvfrom(sock, reply, sizeof(reply), 0, NULL, NULL) < 16) {
    fprintf(stderr, "  thread %ld: no answer to connect\n", index);
    close(sock);
    return NULL;
  }
  connection_id = get64(reply + 8);

  /* Keep g_window announces outstanding: send that many, then drain what has
     come back, rather than waiting for each reply in turn */
  while (atomic_load_explicit(&g_running, memory_order_relaxed)) {
    ssize_t got;
    int     i;
    long    outstanding;

    for (outstanding = 0; outstanding < g_window; ++outstanding) {
    memset(request, 0, sizeof(request));
    put64(request, connection_id);
    put32(request + 8, 1); /* announce */
    put32(request + 12, (uint32_t)index);
    /* One of a fixed set, so that after the first pass almost every announce
       renews a peer rather than creating a torrent. The index is hashed into
       all twenty bytes so the buckets, which key off the first four, are
       spread rather than walked in order. */
    {
      unsigned which = (unsigned)((seed = seed * 1103515245u + 12345u) >> 8) % (unsigned)g_torrents;
      unsigned spread;
      for (i = 0; i < 20; ++i) {
        spread             = which * 2654435761u + (unsigned)i * 40503u;
        spread            ^= spread >> 13;
        request[16 + i]    = (unsigned char)(spread >> 11);
      }
    }
    for (i = 0; i < 20; ++i)
      request[36 + i] = (unsigned char)('a' + (i % 26));
    put64(request + 56, 0);                             /* downloaded */
    put64(request + 64, 0);                             /* left: a seeder */
    put64(request + 72, 0);                             /* uploaded */
    put32(request + 80, 0);                             /* event */
    put32(request + 84, 0);                             /* ip */
    put32(request + 88, (uint32_t)index);               /* key */
    put32(request + 92, (uint32_t)-1);                  /* num_want */
    /* A port of this thread's own, so each thread is a distinct peer and the
       swarms hold more than one */
    request[96] = (unsigned char)((6881 + index) >> 8);
    request[97] = (unsigned char)((6881 + index) & 0xff);

    if (sendto(sock, request, 98, 0, (struct sockaddr *)&g_tracker, sizeof(g_tracker)) != 98)
      break;
    atomic_fetch_add_explicit(&g_sent, 1, memory_order_relaxed);
    }

    for (outstanding = 0; outstanding < g_window; ++outstanding) {
      got = recvfrom(sock, reply, sizeof(reply), 0, NULL, NULL);
      if (got >= 20)
        atomic_fetch_add_explicit(&g_replies, 1, memory_order_relaxed);
      else {
        atomic_fetch_add_explicit(&g_timeouts, 1, memory_order_relaxed);
        break; /* nothing more is coming; go round and send again */
      }
    }
  }

  close(sock);
  return NULL;
}

int main(int argc, char **argv) {
  const char *host    = argc > 1 ? argv[1] : "127.0.0.1";
  int         port    = argc > 2 ? atoi(argv[2]) : 6969;
  long        threads = argc > 3 ? atol(argv[3]) : 16;
  double      run_for = argc > 4 ? atof(argv[4]) : 5.0;
  long        torrents = argc > 5 ? atol(argv[5]) : 10000;
  long        window   = argc > 6 ? atol(argv[6]) : 64;
  pthread_t  *ids     = calloc((size_t)threads, sizeof(pthread_t));
  long        i;
  double      started, elapsed;

  g_torrents = torrents > 0 ? torrents : 1;
  g_window   = window > 0 ? window : 1;

  memset(&g_tracker, 0, sizeof(g_tracker));
  g_tracker.sin_family = AF_INET;
  g_tracker.sin_port   = htons((uint16_t)port);
  if (inet_pton(AF_INET, host, &g_tracker.sin_addr) != 1) {
    fprintf(stderr, "cannot parse address %s\n", host);
    return 1;
  }

  for (i = 0; i < threads; ++i)
    pthread_create(ids + i, NULL, loader, (void *)i);

  started = seconds();
  usleep((useconds_t)(run_for * 1e6));
  atomic_store(&g_running, 0);
  elapsed = seconds() - started;

  for (i = 0; i < threads; ++i)
    pthread_join(ids[i], NULL);

  printf("%.0f replies/s  (%.0f sent/s, %lld replies, %lld lost, %ld threads x %ld in flight, %.1f s)\n",
         (double)atomic_load(&g_replies) / elapsed, (double)atomic_load(&g_sent) / elapsed,
         (long long)atomic_load(&g_replies), (long long)atomic_load(&g_timeouts), threads, g_window, elapsed);
  free(ids);
  return 0;
}