9c6d1b24d42e79728f4672824fc99b38f675332b
[citadel.git] / citadel / utils / stress.c
1
2 /* This message is exactly 1024 bytes */
3 char* const message =
4 "The point of this little file is to stress test a Citadel server.\n"
5 "It spawns n threads, where n is a command line parameter, each of\n"
6 "which writes 1000 messages total to the server.\n"
7 "\n"
8 "-n is a command line parameter indicating how many users to simulate\n"
9 "(default 100).  WARNING: Your system must be capable of creating this\n"
10 "many threads!\n"
11 "\n"
12 "-w is a command line parameter indicating how long to wait in seconds\n"
13 "between posting each message (default 10).  The actual interval\n"
14 "will be randomized between w / 3 and w * 3.\n"
15 "\n"
16 "A run is expected to take approximately three hours, given default\n"
17 "values, and assuming the server can keep up.  If the run takes much\n"
18 "longer than this, there may be a performance problem with the server.\n"
19 "For best results, the test should be run from a different machine than\n"
20 "the server, but connected via a fast network link (e.g. 100Base-T).\n"
21 "\n"
22 "To get baseline results, run the test with -n 1 (simulating 1 user)\n"
23 "on a machine with no other users logged in.\n"
24 "\n"
25 "Example:\n"
26 "stress -n 500 -w 25 myserver > stress.csv\n";
27
28 /* The program tries to be as small and as fast as possible.  Wherever
29  * possible, we avoid allocating memory on the heap.  We do not pass data
30  * between threads.  We do only a minimal amount of calculation.  In
31  * particular, we only output raw timing data for the run; we do not
32  * collate it, average it, or do anything else with it.  See below.
33  * The program does, however, use the same CtdlIPC functions as the
34  * standard Citadel text client, and incurs the same overhead as that
35  * program, using those functions.
36  *
37  * The program first creates a new user with a randomized username which
38  * begins with "testuser".  It then creates 100 rooms named test0 through
39  * test99.  If they already exist, this condition is ignored.
40  *
41  * The program then creates n threads, all of which wait on a conditional
42  * before they do anything.  Once all of the threads have been created,
43  * they are signaled, and begin execution.  Each thread logs in to the
44  * Citadel server separately, simulating a user login, then takes a
45  * timestamp from the operating system.
46  *
47  * Each thread selects a room from 0-99 randomly, then writes a small
48  * (1KB) test message to that room.  1K was chosen because it seems to
49  * represent an average message size for messages we expect to see.
50  * After writing the message, the thread sleeps for w seconds (sleep(w);)
51  * and repeats the process, until it has written 1,000 messages.  The
52  * program provides a status display to standard error, unless w <= 2, in
53  * which case status display is disabled.
54  *
55  * After posting all messages, each thread takes a second timestamp, and
56  * subtracts the first timestamp.  The resulting value (in seconds) is
57  * sent to standard output, followed by the minimum, average, and maximum
58  * amounts of time (in milliseconds) it took to post a message.  The
59  * thread then exits.
60  *
61  * Once all threads have exited, the program exits.
62  */
63
64 #include <stdlib.h>
65 #include <unistd.h>
66 #include <stdio.h>
67 #include <sys/types.h>
68 #include <string.h>
69 #include <libcitadel.h>
70 #include "sysdep.h"
71 #if TIME_WITH_SYS_TIME
72 # include <sys/time.h>
73 # include <time.h>
74 #else
75 # if HAVE_SYS_TIME_H
76 #  include <sys/time.h>
77 # else
78 #  include <time.h>
79 # endif
80 #endif
81 #include "citadel_ipc.h"
82
83 #ifndef HAVE_PTHREAD_H
84 #error This program requires threads
85 #endif
86
87 static int w = 10;              /* see above */
88 static int n = 100;             /* see above */
89 static int m = 1000;            /* Number of messages to send; see above */
90 static volatile int count = 0;  /* Total count of messages posted */
91 static volatile int total = 0;  /* Total messages to be posted */
92 static pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
93 static pthread_mutex_t arg_mutex = PTHREAD_MUTEX_INITIALIZER;
94 static pthread_mutex_t output_mutex = PTHREAD_MUTEX_INITIALIZER;
95
96 static char username[12];
97 static char password[12];
98
99 /*
100  * Mutex for the random number generator
101  * We don't assume that rand_r() is present, so we have to
102  * provide our own locking for rand()
103  */
104 static pthread_mutex_t rand_mutex = PTHREAD_MUTEX_INITIALIZER;
105
106 /*
107  * Conditional.  All the threads wait for this signal to actually
108  * start bombarding the server.
109  */
110 static pthread_mutex_t start_mutex = PTHREAD_MUTEX_INITIALIZER;
111 static pthread_cond_t start_cond = PTHREAD_COND_INITIALIZER;
112
113
114 /*
115  * This is the worker thread.  It logs in and creates the 1,000 messages
116  * as described above.
117  */
118 void* worker(void* data)
119 {
120         CtdlIPC* ipc;   /* My connection to the server */
121         void** args;    /* Args sent in */
122         int r;          /* IPC return code */
123         char aaa[SIZ];  /* Generic buffer */
124         int c;          /* Message count */
125         time_t start, end;      /* Timestamps */
126         struct ctdlipcmessage msg;      /* The message we will post */
127         int argc_;
128         char** argv_;
129         long tmin = LONG_MAX, trun = 0, tmax = LONG_MIN;
130
131         args = (void*)data;
132         argc_ = (int)args[0];
133         argv_ = (char**)args[1];
134
135         /* Setup the message we will be posting */
136         msg.text = message;
137         msg.anonymous = 0;
138         msg.type = 1;
139         strcpy(msg.recipient, "");
140         strcpy(msg.subject, "Test message; ignore");
141         strcpy(msg.author, username);
142
143         pthread_mutex_lock(&arg_mutex);
144         ipc = CtdlIPC_new(argc_, argv_, NULL, NULL);
145         pthread_mutex_unlock(&arg_mutex);
146         if (!ipc)
147                 return NULL;    /* oops, something happened... */
148
149         CtdlIPC_chat_recv(ipc, aaa);
150         if (aaa[0] != '2') {
151                 fprintf(stderr, "Citadel refused me: %s\n", &aaa[4]);
152                 return NULL;    /* server ran out of connections maybe? */
153         }
154
155         CtdlIPCIdentifySoftware(ipc, 8, 8, REV_LEVEL, "Citadel stress tester",
156                 "localhost", aaa);      /* we're lying, the server knows */
157         
158         r = CtdlIPCQueryUsername(ipc, username, aaa);
159         if (r / 100 == 2) {
160                 /* testuser already exists (from previous run?) */
161                 r = CtdlIPCTryLogin(ipc, username, aaa);
162                 if (r / 100 != 3) {
163                         fprintf(stderr, "Citadel refused username: %s\n", aaa);
164                         CtdlIPC_delete_ptr(&ipc);
165                         return NULL;    /* Gawd only knows what went wrong */
166                 }
167                 r = CtdlIPCTryPassword(ipc, password, aaa);
168                 if (r / 100 != 2) {
169                         fprintf(stderr, "Citadel refused password: %s\n", aaa);
170                         CtdlIPC_delete_ptr(&ipc);
171                         return NULL;    /* Gawd only knows what went wrong */
172                 }
173         } else {
174                 /* testuser doesn't yet exist */
175                 r = CtdlIPCCreateUser(ipc, username, 1, aaa);
176                 if (r / 100 != 2) {
177                         fprintf(stderr, "Citadel refused create user: %s\n", aaa);
178                         CtdlIPC_delete_ptr(&ipc);
179                         return NULL;    /* Gawd only knows what went wrong */
180                 }
181                 r = CtdlIPCChangePassword(ipc, password, aaa);
182                 if (r / 100 != 2) {
183                         fprintf(stderr, "Citadel refused change password: %s\n", aaa);
184                         CtdlIPC_delete_ptr(&ipc);
185                         return NULL;    /* Gawd only knows what went wrong */
186                 }
187         }
188
189         /* Wait for the rest of the threads */
190         pthread_mutex_lock(&start_mutex);
191         pthread_cond_wait(&start_cond, &start_mutex);
192         pthread_mutex_unlock(&start_mutex);
193
194         /* And now the fun begins!  Send out a whole shitload of messages */
195         start = time(NULL);
196         for (c = 0; c < m; c++) {
197                 int rm;
198                 char room[7];
199                 struct ctdlipcroom *rret;
200                 struct timeval tv;
201                 long tstart, tend;
202                 int wait;
203
204                 /* Wait for a while */
205                 pthread_mutex_lock(&rand_mutex);
206                 /* See Numerical Recipes in C or Knuth vol. 2 ch. 3 */
207                 /* Randomize between w/3 to w*3 (yes, it's complicated) */
208                 wait = (int)((1.0+2.7*(float)w)*rand()/(RAND_MAX+(float)w/3.0)); /* range 0-99 */
209                 pthread_mutex_unlock(&rand_mutex);
210                 sleep(wait);
211
212                 /* Select the room to goto */
213                 pthread_mutex_lock(&rand_mutex);
214                 /* See Numerical Recipes in C or Knuth vol. 2 ch. 3 */
215                 rm = (int)(100.0*rand()/(RAND_MAX+1.0)); /* range 0-99 */
216                 pthread_mutex_unlock(&rand_mutex);
217
218                 /* Goto the selected room */
219                 sprintf(room, "test%d", rm);
220                 /* Create the room if not existing. Ignore the return */
221                 r = CtdlIPCCreateRoom(ipc, 1, room, 0, NULL, 0, aaa);
222                 if (r / 100 != 2 && r != 574) { /* Already exists */
223                         fprintf(stderr, "Citadel refused room create: %s\n", aaa);
224                         pthread_mutex_lock(&count_mutex);
225                         total -= m - c;
226                         pthread_mutex_unlock(&count_mutex);
227                         CtdlIPC_delete_ptr(&ipc);
228                         return NULL;
229                 }
230                 gettimeofday(&tv, NULL);
231                 tstart = tv.tv_sec * 1000 + tv.tv_usec / 1000; /* cvt to msec */
232                 r = CtdlIPCGotoRoom(ipc, room, "", &rret, aaa);
233                 if (r / 100 != 2) {
234                         fprintf(stderr, "Citadel refused room change: %s\n", aaa);
235                         pthread_mutex_lock(&count_mutex);
236                         total -= m - c;
237                         pthread_mutex_unlock(&count_mutex);
238                         CtdlIPC_delete_ptr(&ipc);
239                         return NULL;
240                 }
241
242                 /* Post the message */
243                 r = CtdlIPCPostMessage(ipc, 1, NULL, &msg, aaa);
244                 if (r / 100 != 4) {
245                         fprintf(stderr, "Citadel refused message entry: %s\n", aaa);
246                         pthread_mutex_lock(&count_mutex);
247                         total -= m - c;
248                         pthread_mutex_unlock(&count_mutex);
249                         CtdlIPC_delete_ptr(&ipc);
250                         return NULL;
251                 }
252
253                 /* Do a status update */
254                 pthread_mutex_lock(&count_mutex);
255                 count++;
256                 pthread_mutex_unlock(&count_mutex);
257                 fprintf(stderr, " %d/%d=%d%%             \r",
258                         count, total,
259                         (int)(100 * count / total));
260                 gettimeofday(&tv, NULL);
261                 tend = tv.tv_sec * 1000 + tv.tv_usec / 1000; /* cvt to msec */
262                 tend -= tstart;
263                 if (tend < tmin) tmin = tend;
264                 if (tend > tmax) tmax = tend;
265                 trun += tend;
266         }
267         end = time(NULL);
268         pthread_mutex_lock(&output_mutex);
269         fprintf(stderr, "               \r");
270         printf("%ld %ld %ld %ld\n", end - start, tmin, trun / c, tmax);
271         pthread_mutex_unlock(&output_mutex);
272         return (void*)(end - start);
273 }
274
275
276 /*
277  * Shift argument list
278  */
279 int shift(int argc, char **argv, int start, int count)
280 {
281         int i;
282
283         for (i = start; i < argc - count; ++i)
284                 argv[i] = argv[i + count];
285         return argc - count;
286 }
287
288
289 /*
290  * Main loop.  Start a shitload of threads, all of which will attempt to
291  * kick a Citadel server square in the nuts.
292  */
293 int main(int argc, char** argv)
294 {
295         void* data[2];          /* pass args to worker thread */
296         pthread_t* threads;     /* A shitload of threads */
297         pthread_attr_t attr;    /* Thread attributes (we use defaults) */
298         int i;                  /* Counters */
299         long runtime;           /* Run time for each thread */
300
301         /* Read argument list */
302         for (i = 0; i < argc; i++) {
303                 if (!strcmp(argv[i], "-n")) {
304                         n = atoi(argv[i + 1]);
305                         argc = shift(argc, argv, i, 2);
306                 }
307                 if (!strcmp(argv[i], "-w")) {
308                         w = atoi(argv[i + 1]);
309                         argc = shift(argc, argv, i, 2);
310                 }
311                 if (!strcmp(argv[i], "-m")) {
312                         m = atoi(argv[i + 1]);
313                         argc = shift(argc, argv, i, 2);
314                 }
315                 if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
316                         fprintf(stderr, "Read stress.c for usage info\n");
317                         return 1;
318                 }
319         }
320
321         data[0] = (void*)argc;  /* pass args to worker thread */
322         data[1] = (void*)argv;  /* pass args to worker thread */
323
324         /* This is how many total messages will be posted */
325         total = n * m;
326
327         /* Pick a randomized username */
328         pthread_mutex_lock(&rand_mutex);
329         /* See Numerical Recipes in C or Knuth vol. 2 ch. 3 */
330         i = (int)(100.0*rand()/(RAND_MAX+1.0)); /* range 0-99 */
331         pthread_mutex_unlock(&rand_mutex);
332         sprintf(username, "testuser%d", i);
333         strcpy(password, username);
334
335         /* First, memory for our shitload of threads */
336         threads = calloc(n, sizeof(pthread_t));
337         if (!threads) {
338                 perror("Not enough memory");
339                 return 1;
340         }
341
342         /* Then thread attributes (all defaults for now) */
343         pthread_attr_init(&attr);
344         pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
345
346         /* Then, create some threads */
347         fprintf(stderr, "Creating threads      \r");
348         for (i = 0; i < n; ++i) {
349                 pthread_create(&threads[i], &attr, worker, (void*)data);
350                 
351                 /* Give thread #0 time to create the user account */
352                 if (i == 0) sleep(3);
353         }
354
355         //fprintf(stderr, "Starting in %d seconds\r", n);
356         //sleep(n);
357         fprintf(stderr, "                      \r");
358
359         /* Then, signal the conditional they all are waiting on */
360         pthread_mutex_lock(&start_mutex);
361         pthread_cond_broadcast(&start_cond);
362         pthread_mutex_unlock(&start_mutex);
363
364         /* Then wait for them to exit */
365         for (i = 0; i < n; i++) {
366                 pthread_join(threads[i], (void*)&runtime);
367                 /* We're ignoring this value for now... TODO */
368         }
369         fprintf(stderr, "\r                                                                               \r");
370         return 0;
371 }