2655b11c9a8deb06c779c510b03c968ed91cdef2
[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 #include <time.h>
72 #include "citadel_ipc.h"
73
74 #ifndef HAVE_PTHREAD_H
75 #error This program requires threads
76 #endif
77
78 static int w = 10;              /* see above */
79 static int n = 100;             /* see above */
80 static int m = 1000;            /* Number of messages to send; see above */
81 static volatile int count = 0;  /* Total count of messages posted */
82 static volatile int total = 0;  /* Total messages to be posted */
83 static pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
84 static pthread_mutex_t arg_mutex = PTHREAD_MUTEX_INITIALIZER;
85 static pthread_mutex_t output_mutex = PTHREAD_MUTEX_INITIALIZER;
86
87 static char username[12];
88 static char password[12];
89
90 /*
91  * Mutex for the random number generator
92  * We don't assume that rand_r() is present, so we have to
93  * provide our own locking for rand()
94  */
95 static pthread_mutex_t rand_mutex = PTHREAD_MUTEX_INITIALIZER;
96
97 /*
98  * Conditional.  All the threads wait for this signal to actually
99  * start bombarding the server.
100  */
101 static pthread_mutex_t start_mutex = PTHREAD_MUTEX_INITIALIZER;
102 static pthread_cond_t start_cond = PTHREAD_COND_INITIALIZER;
103
104
105 /*
106  * This is the worker thread.  It logs in and creates the 1,000 messages
107  * as described above.
108  */
109 void* worker(void* data)
110 {
111         CtdlIPC* ipc;   /* My connection to the server */
112         void** args;    /* Args sent in */
113         int r;          /* IPC return code */
114         char aaa[SIZ];  /* Generic buffer */
115         int c;          /* Message count */
116         time_t start, end;      /* Timestamps */
117         struct ctdlipcmessage msg;      /* The message we will post */
118         int argc_;
119         char** argv_;
120         long tmin = LONG_MAX, trun = 0, tmax = LONG_MIN;
121
122         args = (void*)data;
123         argc_ = (int)args[0];
124         argv_ = (char**)args[1];
125
126         /* Setup the message we will be posting */
127         msg.text = message;
128         msg.anonymous = 0;
129         msg.type = 1;
130         strcpy(msg.recipient, "");
131         strcpy(msg.subject, "Test message; ignore");
132         strcpy(msg.author, username);
133
134         pthread_mutex_lock(&arg_mutex);
135         ipc = CtdlIPC_new(argc_, argv_, NULL, NULL);
136         pthread_mutex_unlock(&arg_mutex);
137         if (!ipc)
138                 return NULL;    /* oops, something happened... */
139
140         CtdlIPC_chat_recv(ipc, aaa);
141         if (aaa[0] != '2') {
142                 fprintf(stderr, "Citadel refused me: %s\n", &aaa[4]);
143                 return NULL;    /* server ran out of connections maybe? */
144         }
145
146         CtdlIPCIdentifySoftware(ipc, 8, 8, REV_LEVEL, "Citadel stress tester",
147                 "localhost", aaa);      /* we're lying, the server knows */
148         
149         r = CtdlIPCQueryUsername(ipc, username, aaa);
150         if (r / 100 == 2) {
151                 /* testuser already exists (from previous run?) */
152                 r = CtdlIPCTryLogin(ipc, username, aaa);
153                 if (r / 100 != 3) {
154                         fprintf(stderr, "Citadel refused username: %s\n", aaa);
155                         CtdlIPC_delete_ptr(&ipc);
156                         return NULL;    /* Gawd only knows what went wrong */
157                 }
158                 r = CtdlIPCTryPassword(ipc, password, aaa);
159                 if (r / 100 != 2) {
160                         fprintf(stderr, "Citadel refused password: %s\n", aaa);
161                         CtdlIPC_delete_ptr(&ipc);
162                         return NULL;    /* Gawd only knows what went wrong */
163                 }
164         } else {
165                 /* testuser doesn't yet exist */
166                 r = CtdlIPCCreateUser(ipc, username, 1, aaa);
167                 if (r / 100 != 2) {
168                         fprintf(stderr, "Citadel refused create user: %s\n", aaa);
169                         CtdlIPC_delete_ptr(&ipc);
170                         return NULL;    /* Gawd only knows what went wrong */
171                 }
172                 r = CtdlIPCChangePassword(ipc, password, aaa);
173                 if (r / 100 != 2) {
174                         fprintf(stderr, "Citadel refused change password: %s\n", aaa);
175                         CtdlIPC_delete_ptr(&ipc);
176                         return NULL;    /* Gawd only knows what went wrong */
177                 }
178         }
179
180         /* Wait for the rest of the threads */
181         pthread_mutex_lock(&start_mutex);
182         pthread_cond_wait(&start_cond, &start_mutex);
183         pthread_mutex_unlock(&start_mutex);
184
185         /* And now the fun begins!  Send out a whole shitload of messages */
186         start = time(NULL);
187         for (c = 0; c < m; c++) {
188                 int rm;
189                 char room[7];
190                 struct ctdlipcroom *rret;
191                 struct timeval tv;
192                 long tstart, tend;
193                 int wait;
194
195                 /* Wait for a while */
196                 pthread_mutex_lock(&rand_mutex);
197                 /* See Numerical Recipes in C or Knuth vol. 2 ch. 3 */
198                 /* Randomize between w/3 to w*3 (yes, it's complicated) */
199                 wait = (int)((1.0+2.7*(float)w)*rand()/(RAND_MAX+(float)w/3.0)); /* range 0-99 */
200                 pthread_mutex_unlock(&rand_mutex);
201                 sleep(wait);
202
203                 /* Select the room to goto */
204                 pthread_mutex_lock(&rand_mutex);
205                 /* See Numerical Recipes in C or Knuth vol. 2 ch. 3 */
206                 rm = (int)(100.0*rand()/(RAND_MAX+1.0)); /* range 0-99 */
207                 pthread_mutex_unlock(&rand_mutex);
208
209                 /* Goto the selected room */
210                 sprintf(room, "test%d", rm);
211                 /* Create the room if not existing. Ignore the return */
212                 r = CtdlIPCCreateRoom(ipc, 1, room, 0, NULL, 0, aaa);
213                 if (r / 100 != 2 && r != 574) { /* Already exists */
214                         fprintf(stderr, "Citadel refused room create: %s\n", aaa);
215                         pthread_mutex_lock(&count_mutex);
216                         total -= m - c;
217                         pthread_mutex_unlock(&count_mutex);
218                         CtdlIPC_delete_ptr(&ipc);
219                         return NULL;
220                 }
221                 gettimeofday(&tv, NULL);
222                 tstart = tv.tv_sec * 1000 + tv.tv_usec / 1000; /* cvt to msec */
223                 r = CtdlIPCGotoRoom(ipc, room, "", &rret, aaa);
224                 if (r / 100 != 2) {
225                         fprintf(stderr, "Citadel refused room change: %s\n", aaa);
226                         pthread_mutex_lock(&count_mutex);
227                         total -= m - c;
228                         pthread_mutex_unlock(&count_mutex);
229                         CtdlIPC_delete_ptr(&ipc);
230                         return NULL;
231                 }
232
233                 /* Post the message */
234                 r = CtdlIPCPostMessage(ipc, 1, NULL, &msg, aaa);
235                 if (r / 100 != 4) {
236                         fprintf(stderr, "Citadel refused message entry: %s\n", aaa);
237                         pthread_mutex_lock(&count_mutex);
238                         total -= m - c;
239                         pthread_mutex_unlock(&count_mutex);
240                         CtdlIPC_delete_ptr(&ipc);
241                         return NULL;
242                 }
243
244                 /* Do a status update */
245                 pthread_mutex_lock(&count_mutex);
246                 count++;
247                 pthread_mutex_unlock(&count_mutex);
248                 fprintf(stderr, " %d/%d=%d%%             \r",
249                         count, total,
250                         (int)(100 * count / total));
251                 gettimeofday(&tv, NULL);
252                 tend = tv.tv_sec * 1000 + tv.tv_usec / 1000; /* cvt to msec */
253                 tend -= tstart;
254                 if (tend < tmin) tmin = tend;
255                 if (tend > tmax) tmax = tend;
256                 trun += tend;
257         }
258         end = time(NULL);
259         pthread_mutex_lock(&output_mutex);
260         fprintf(stderr, "               \r");
261         printf("%ld %ld %ld %ld\n", end - start, tmin, trun / c, tmax);
262         pthread_mutex_unlock(&output_mutex);
263         return (void*)(end - start);
264 }
265
266
267 /*
268  * Shift argument list
269  */
270 int shift(int argc, char **argv, int start, int count)
271 {
272         int i;
273
274         for (i = start; i < argc - count; ++i)
275                 argv[i] = argv[i + count];
276         return argc - count;
277 }
278
279
280 /*
281  * Main loop.  Start a shitload of threads, all of which will attempt to
282  * kick a Citadel server square in the nuts.
283  */
284 int main(int argc, char** argv)
285 {
286         void* data[2];          /* pass args to worker thread */
287         pthread_t* threads;     /* A shitload of threads */
288         pthread_attr_t attr;    /* Thread attributes (we use defaults) */
289         int i;                  /* Counters */
290         long runtime;           /* Run time for each thread */
291
292         /* Read argument list */
293         for (i = 0; i < argc; i++) {
294                 if (!strcmp(argv[i], "-n")) {
295                         n = atoi(argv[i + 1]);
296                         argc = shift(argc, argv, i, 2);
297                 }
298                 if (!strcmp(argv[i], "-w")) {
299                         w = atoi(argv[i + 1]);
300                         argc = shift(argc, argv, i, 2);
301                 }
302                 if (!strcmp(argv[i], "-m")) {
303                         m = atoi(argv[i + 1]);
304                         argc = shift(argc, argv, i, 2);
305                 }
306                 if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
307                         fprintf(stderr, "Read stress.c for usage info\n");
308                         return 1;
309                 }
310         }
311
312         data[0] = (void*)argc;  /* pass args to worker thread */
313         data[1] = (void*)argv;  /* pass args to worker thread */
314
315         /* This is how many total messages will be posted */
316         total = n * m;
317
318         /* Pick a randomized username */
319         pthread_mutex_lock(&rand_mutex);
320         /* See Numerical Recipes in C or Knuth vol. 2 ch. 3 */
321         i = (int)(100.0*rand()/(RAND_MAX+1.0)); /* range 0-99 */
322         pthread_mutex_unlock(&rand_mutex);
323         sprintf(username, "testuser%d", i);
324         strcpy(password, username);
325
326         /* First, memory for our shitload of threads */
327         threads = calloc(n, sizeof(pthread_t));
328         if (!threads) {
329                 perror("Not enough memory");
330                 return 1;
331         }
332
333         /* Then thread attributes (all defaults for now) */
334         pthread_attr_init(&attr);
335         pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
336
337         /* Then, create some threads */
338         fprintf(stderr, "Creating threads      \r");
339         for (i = 0; i < n; ++i) {
340                 pthread_create(&threads[i], &attr, worker, (void*)data);
341                 
342                 /* Give thread #0 time to create the user account */
343                 if (i == 0) sleep(3);
344         }
345
346         //fprintf(stderr, "Starting in %d seconds\r", n);
347         //sleep(n);
348         fprintf(stderr, "                      \r");
349
350         /* Then, signal the conditional they all are waiting on */
351         pthread_mutex_lock(&start_mutex);
352         pthread_cond_broadcast(&start_cond);
353         pthread_mutex_unlock(&start_mutex);
354
355         /* Then wait for them to exit */
356         for (i = 0; i < n; i++) {
357                 pthread_join(threads[i], (void*)&runtime);
358                 /* We're ignoring this value for now... TODO */
359         }
360         fprintf(stderr, "\r                                                                               \r");
361         return 0;
362 }