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