6ed6e979f453cfcbcce035da77adb18851e00ce3
[citadel.git] / citadel / utils / loadtest.c
1 // Load testing utility for Citadel Server
2 //
3 // Copyright (c) 1987-2024 by the citadel.org team
4 //
5 // This program is open source software.  Use, duplication, or disclosure
6 // is subject to the terms of the GNU General Public License, version 3.
7
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <unistd.h>
11 #include <sys/types.h>
12 #include <sys/wait.h>
13 #include <string.h>
14 #include <fcntl.h>
15 #include <stdio.h>
16 #include <ctype.h>
17 #include <signal.h>
18 #include <errno.h>
19 #include <limits.h>
20 #include <sys/socket.h>
21 #include <sys/un.h>
22 #include <pthread.h>
23 #include "../server/citadel_defs.h"
24 #include "../server/server.h"
25 #include "../server/citadel_dirs.h"
26 #include <libcitadel.h>
27
28 char ctdldir[PATH_MAX]=CTDLDIR;
29
30 char *words[] = {
31         "lorem","ipsum","dolor","sit","amet","consectetuer","adipiscing","elit","integer","in","mi","a","mauris",
32         "ornare","sagittis","suspendisse","potenti","suspendisse","dapibus","dignissim","dolor","nam",
33         "sapien","tellus","tempus","et","tempus","ac","tincidunt","in","arcu","duis","dictum","proin","magna",
34         "nulla","pellentesque","non","commodo","et","iaculis","sit","amet","mi","mauris","condimentum","massa",
35         "ut","metus","donec","viverra","sapien","mattis","rutrum","tristique","lacus","eros","semper","tellus",
36         "et","molestie","nisi","sapien","eu","massa","vestibulum","ante","ipsum","primis","in","faucibus","orci",
37         "luctus","et","ultrices","posuere","cubilia","curae;","fusce","erat","tortor","mollis","ut","accumsan",
38         "ut","lacinia","gravida","libero","curabitur","massa","felis","accumsan","feugiat","convallis","sit",
39         "amet","porta","vel","neque","duis","et","ligula","non","elit","ultricies","rutrum","suspendisse",
40         "tempor","quisque","posuere","malesuada","velit","sed","pellentesque","mi","a","purus","integer",
41         "imperdiet","orci","a","eleifend","mollis","velit","nulla","iaculis","arcu","eu","rutrum","magna","quam",
42         "sed","elit","nullam","egestas","integer","interdum","purus","nec","mauris","vestibulum","ac","mi","in",
43         "nunc","suscipit","dapibus","duis","consectetuer","ipsum","et","pharetra","sollicitudin","metus",
44         "turpis","facilisis","magna","vitae","dictum","ligula","nulla","nec","mi","nunc","ante","urna","gravida",
45         "sit","amet","congue","et","accumsan","vitae","magna","praesent","luctus","nullam","in","velit",
46         "praesent","est","curabitur","turpis","class","aptent","taciti","sociosqu","ad","litora","torquent",
47         "per","conubia","nostra","per","inceptos","hymenaeos","cras","consectetuer","nibh","in","lacinia",
48         "ornare","turpis","sem","tempor","massa","sagittis","feugiat","mauris","nibh","non","tellus",
49         "phasellus","mi","fusce","enim","mauris","ultrices","turpis","eu","adipiscing","viverra","justo",
50         "libero","ullamcorper","massa","id","ultrices","velit","est","quis","tortor","quisque","condimentum",
51         "lacus","volutpat","nonummy","accumsan","est","nunc","imperdiet","magna","vulputate","aliquet","nisi",
52         "risus","at","est","aliquam","imperdiet","gravida","tortor","praesent","interdum","accumsan","ante",
53         "vivamus","est","ligula","consequat","sed","pulvinar","eu","consequat","vitae","eros","nulla","elit",
54         "nunc","congue","eget","scelerisque","a","tempor","ac","nisi","morbi","facilisis","pellentesque",
55         "habitant","morbi","tristique","senectus","et","netus","et","malesuada","fames","ac","turpis","egestas",
56         "in","hac","habitasse","platea","dictumst","suspendisse","vel","lorem","ut","ligula","tempor",
57         "consequat","quisque","consectetuer","nisl","eget","elit","proin","quis","mauris","ac","orci",
58         "accumsan","suscipit","sed","ipsum","sed","vel","libero","nec","elit","feugiat","blandit","vestibulum",
59         "purus","nulla","accumsan","et","volutpat","at","pellentesque","vel","urna","suspendisse","nonummy",
60         "aliquam","pulvinar","libero","donec","vulputate","orci","ornare","bibendum","condimentum","lorem",
61         "elit","dignissim","sapien","ut","aliquam","nibh","augue","in","turpis","phasellus","ac","eros",
62         "praesent","luctus","lorem","a","mollis","lacinia","leo","turpis","commodo","sem","in","lacinia","mi",
63         "quam","et","quam","curabitur","a","libero","vel","tellus","mattis","imperdiet","in","congue","neque","ut",
64         "scelerisque","bibendum","libero","lacus","ullamcorper","sapien","quis","aliquet","massa","velit",
65         "vel","orci","fusce","in","nulla","quis","est","cursus","gravida","in","nibh","lorem","ipsum","dolor","sit",
66         "amet","consectetuer","adipiscing","elit","integer","fermentum","pretium","massa","morbi","feugiat",
67         "iaculis","nunc","aenean","aliquam","pretium","orci","cum","sociis","natoque","penatibus","et","magnis",
68         "dis","parturient","montes","nascetur","ridiculus","mus","vivamus","quis","tellus","vel","quam",
69         "varius","bibendum","fusce","est","metus","feugiat","at","porttitor","et","cursus","quis","pede","nam","ut",
70         "augue","nulla","posuere","phasellus","at","dolor","a","enim","cursus","vestibulum","duis","id","nisi",
71         "duis","semper","tellus","ac","nulla","vestibulum","scelerisque","lobortis","dolor","aenean","a",
72         "felis","aliquam","erat","volutpat","donec","a","magna","vitae","pede","sagittis","lacinia","cras",
73         "vestibulum","diam","ut","arcu","mauris","a","nunc","duis","sollicitudin","erat","sit","amet","turpis",
74         "proin","at","libero","eu","diam","lobortis","fermentum","nunc","lorem","turpis","imperdiet","id",
75         "gravida","eget","aliquet","sed","purus","ut","vehicula","laoreet","ante","mauris","eu","nunc","sed","sit",
76         "amet","elit","nec","ipsum","aliquam","egestas","donec","non","nibh","cras","sodales","pretium","massa",
77         "praesent","hendrerit","est","et","risus","vivamus","eget","pede","curabitur","tristique",
78         "scelerisque","dui","nullam","ullamcorper","vivamus","venenatis","velit","eget","enim","nunc","eu",
79         "nunc","eget","felis","malesuada","fermentum","quisque","magna","mauris","ligula","felis","luctus","a",
80         "aliquet","nec","vulputate","eget","magna","quisque","placerat","diam","sed","arcu","praesent",
81         "sollicitudin","aliquam","non","sapien","quisque","id","augue","class","aptent","taciti","sociosqu",
82         "ad","litora","torquent","per","conubia","nostra","per","inceptos","hymenaeos","etiam","lacus","lectus",
83         "mollis","quis","mattis","nec","commodo","facilisis","nibh","sed","sodales","sapien","ac","ante","duis",
84         "eget","lectus","in","nibh","lacinia","auctor","fusce","interdum","lectus","non","dui","integer",
85         "accumsan","quisque","quam","curabitur","scelerisque","imperdiet","nisl","suspendisse","potenti",
86         "nam","massa","leo","iaculis","sed","accumsan","id","ultrices","nec","velit","suspendisse","potenti",
87         "mauris","bibendum","turpis","ac","viverra","sollicitudin","metus","massa","interdum","orci","non",
88         "imperdiet","orci","ante","at","ipsum","etiam","eget","magna","mauris","at","tortor","eu","lectus",
89         "tempor","tincidunt","phasellus","justo","purus","pharetra","ut","ultricies","nec","consequat","vel",
90         "nisi","fusce","vitae","velit","at","libero","sollicitudin","sodales","aenean","mi","libero","ultrices",
91         "id","suscipit","vitae","dapibus","eu","metus","aenean","vestibulum","nibh","ac","massa","vivamus",
92         "vestibulum","libero","vitae","purus","in","hac","maga","habitasse","platea","dictumst","curabitur",
93         "blandit","nunc","non","arcu","ut","nec","nibh","morbi","quis","leo","vel","magna","commodo","rhoncus",
94         "donec","congue","leo","eu","lacus","pellentesque","at","erat","id","mi","consequat","congue","praesent",
95         "a","nisl","ut","diam","interdum","molestie","fusce","suscipit","rhoncus","sem","donec","pretium",
96         "aliquam","molestie","vivamus","et","justo","at","augue","aliquet","dapibus","pellentesque","felis",
97         "morbi","semper","in","venenatis","imperdiet","neque","donec","auctor","molestie","augue","nulla","id",
98         "arcu","sit","amet","dui","lacinia","convallis","proin","tincidunt","proin","a","ante","nunc","imperdiet",
99         "augue","nullam","sit","amet","arcu","quisque","laoreet","viverra","felis","lorem","ipsum","dolor","sit",
100         "amet","consectetuer","adipiscing","elit","in","hac","habitasse","platea","dictumst","pellentesque",
101         "habitant","morbi","tristique","senectus","et","netus","et","malesuada","fames","ac","turpis","egestas",
102         "class","aptent","taciti","sociosqu","ad","litora","torquent","per","conubia","nostra","per","inceptos",
103         "hymenaeos","nullam","nibh","sapien","volutpat","ut","placerat","quis","ornare","at","lorem","class",
104         "aptent","taciti","sociosqu","ad","litora","torquent","per","conubia","nostra","per","inceptos",
105         "hymenaeos","morbi","dictum","massa","id","libero","ut","neque","phasellus","tincidunt","nibh","ut",
106         "tincidunt","lacinia","lacus","nulla","aliquam","mi","a","interdum","dui","augue","non","pede","duis",
107         "nunc","magna","vulputate","a","porta","at","tincidunt","a","nulla","praesent","facilisis",
108         "suspendisse","sodales","feugiat","purus","cras","et","justo","a","mauris","mollis","imperdiet","morbi",
109         "erat","mi","ultrices","eget","aliquam","elementum","iaculis","id","velit","in","scelerisque","enim",
110         "sit","amet","turpis","sed","aliquam","odio","nonummy","ullamcorper","mollis","lacus","nibh","tempor",
111         "dolor","sit","amet","varius","sem","neque","ac","dui","nunc","et","est","eu","massa","eleifend","mollis",
112         "mauris","aliquet","orci","quis","tellus","ut","mattis","praesent","mollis","consectetuer","quam",
113         "nulla","nulla","nunc","accumsan","nunc","sit","amet","scelerisque","porttitor","nibh","pede","lacinia",
114         "justo","tristique","mattis","purus","eros","non","velit","aenean","sagittis","commodo","erat",
115         "aliquam","id","lacus","morbi","vulputate","vestibulum","elit"
116 };
117 int nwords = sizeof(words) / sizeof(char *);
118
119
120 int uds_connectsock(char *sockpath) {
121         int s;
122         struct sockaddr_un addr;
123
124         memset(&addr, 0, sizeof(addr));
125         addr.sun_family = AF_UNIX;
126         strncpy(addr.sun_path, sockpath, sizeof addr.sun_path);
127
128         s = socket(AF_UNIX, SOCK_STREAM, 0);
129         if (s < 0) {
130                 return(-1);
131         }
132
133         if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
134                 close(s);
135                 return(-1);
136         }
137
138         return(s);
139 }
140
141
142 // input binary data from socket
143 void serv_read(int serv_sock, char *buf, int bytes) {
144         int len, rlen;
145
146         len = 0;
147         while (len < bytes) {
148                 rlen = read(serv_sock, &buf[len], bytes - len);
149                 if (rlen < 1) {
150                         return;
151                 }
152                 len = len + rlen;
153         }
154 }
155
156
157 // send binary to server
158 void serv_write(int serv_sock, char *buf, int nbytes) {
159         int bytes_written = 0;
160         int retval;
161         while (bytes_written < nbytes) {
162                 retval = write(serv_sock, &buf[bytes_written], nbytes - bytes_written);
163                 if (retval < 1) {
164                         return;
165                 }
166                 bytes_written = bytes_written + retval;
167         }
168 }
169
170
171 // input string from socket - implemented in terms of serv_read()
172 void serv_gets(int serv_sock, char *buf) {
173         int i;
174
175         // Read one character at a time.
176         for (i = 0;; i++) {
177                 serv_read(serv_sock, &buf[i], 1);
178                 if (buf[i] == '\n' || i == (SIZ-1))
179                         break;
180         }
181
182         // If we got a long line, discard characters until the newline.
183         if (i == (SIZ-1)) {
184                 while (buf[i] != '\n') {
185                         serv_read(serv_sock, &buf[i], 1);
186                 }
187         }
188
189         // Strip all trailing nonprintables (crlf)
190         buf[i] = 0;
191 }
192
193
194 // send line to server - implemented in terms of serv_write()
195 void serv_puts(int serv_sock, char *buf) {
196         serv_write(serv_sock, buf, strlen(buf));
197         serv_write(serv_sock, "\n", 1);
198 }
199
200
201 char *random_rooms[] = {
202         "Load Testing Test Room One",
203         "Load Test 2: Electric Boogaloo",
204         "Three shall be the Load Testing",
205         "This Is The Fourth Load Test Room",
206         "Five Guys Load Testing and Fries"
207 };
208 int nrooms = sizeof(random_rooms) / sizeof(char *);
209 char *test_user = "Load Test User";
210 char test_pass[16];
211 time_t time_started;
212 volatile int ops_completed;
213
214
215 // These are our randomized load test operations: an even mix of changing rooms, posting messages, and deleting messages.
216 void perform_random_thing(int serv_sock) {
217         int op = random() % 3;
218         char buf[SIZ];
219         int i;
220         int bigness;
221
222         // Random operation 0 : change rooms
223         if (op == 0) {
224                 snprintf(buf, sizeof(buf), "GOTO %s", random_rooms[random() % nrooms]);
225                 serv_puts(serv_sock, buf);
226                 serv_gets(serv_sock, buf);
227         }
228
229         // Random operation 1 : post a message
230         if (op == 1) {
231                 serv_puts(serv_sock, "ENT0 1");
232                 serv_gets(serv_sock, buf);
233                 if (buf[0] == '4') {
234
235                         bigness = random() % 500;
236                         strcpy(buf, "");
237                         for (i=0; i<bigness; ++i) {
238                                 strcat(buf, words[random() % nwords]);
239                                 if ( (i != 0) && ((i % 10) == 0) ) {
240                                         serv_puts(serv_sock, buf);
241                                         strcpy(buf, "");
242                                 }
243                                 else {
244                                         strcat(buf, " ");
245                                 }
246                         }
247                         serv_puts(serv_sock, buf);
248                         serv_puts(serv_sock, "000");
249                 }
250         }
251
252         // Random operation 2 : delete a message
253         int total_msgs;
254         long selected_msg;
255         if (op == 2) {
256                 total_msgs = 0;
257                 selected_msg = 0;
258
259                 do {
260                         serv_puts(serv_sock, "MSGS ALL");
261                         serv_gets(serv_sock, buf);
262                         if (buf[0] == '1') {
263                                 while (serv_gets(serv_sock, buf), strcmp(buf, "000")) {
264                                         ++total_msgs;
265                                         if ((random() % total_msgs) == 0) {
266                                                 selected_msg = atol(buf);
267                                         }
268                                 }
269                         }
270                         snprintf(buf, sizeof buf, "DELE %ld", selected_msg);
271                         serv_puts(serv_sock, buf);
272                         serv_gets(serv_sock, buf);
273                 } while ( (buf[0] != '2') && (total_msgs > 0));
274         }
275 }
276
277 #define ROW_OFFSET 8
278
279 // This is the main loop.  We log in as the load test user, and then perform random operations until stopped.
280 void *loadtest(void *pointer_to_thread_id) {
281         char buf[SIZ];
282         int serv_sock;
283
284         int thread_id = *(int *)pointer_to_thread_id;
285
286         serv_sock = uds_connectsock(file_citadel_socket);
287
288         if (serv_sock < 0) {
289                 printf("\033[8;0H\033[31mWarning: some threads failed to connect to Citadel Server.\033[0m");
290                 fflush(stdout);
291                 pthread_exit(NULL);
292         }
293
294         serv_gets(serv_sock, buf);
295         snprintf(buf, sizeof buf, "USER %s", test_user);
296         serv_puts(serv_sock, buf);
297         serv_gets(serv_sock, buf);
298         snprintf(buf, sizeof buf, "PASS %s", test_pass);
299         serv_puts(serv_sock, buf);
300         serv_gets(serv_sock, buf);
301         snprintf(buf, sizeof(buf), "GOTO %s", random_rooms[0]);
302         serv_puts(serv_sock, buf);
303         serv_gets(serv_sock, buf);
304
305         // Find a nice spot on the screen to show the operation count for this thread.
306         int row = ROW_OFFSET + (thread_id % 20);
307         int col = (thread_id / 20) * 10;
308         long ops = 0;
309         printf("\033[%d;%dH\033[33m       0\033[0m", row, col);
310         fflush(stdout);
311
312         while(1) {
313                 perform_random_thing(serv_sock);
314                 printf("\033[%d;%dH\033[32m%8ld\033[0m", row, col, ++ops);
315                 ++ops_completed;                // this is declared "volatile" so we don't need to lock it
316                 if (thread_id == 0) {
317                         printf("\033[2;55H\033[44m\033[33m\033[1m%d ops/sec \033[0m", (ops_completed / (time(NULL) - time_started)));
318                 }
319                 fflush(stdout);
320         }
321 }
322
323
324 // Create (or replace) the account used for load testing, then create the rooms in which we will load test.
325 void setup_accounts(int serv_sock) {
326         int i;
327         char buf[SIZ];
328
329         snprintf(buf, sizeof buf, "CREU %s", test_user);
330         serv_puts(serv_sock, buf);
331         serv_gets(serv_sock, buf);
332         snprintf(buf, sizeof buf, "ASUP %s|%s|0|||6|", test_user, test_pass);
333         serv_puts(serv_sock, buf);
334         serv_gets(serv_sock, buf);
335         snprintf(buf, sizeof buf, "USER %s", test_user);
336         serv_puts(serv_sock, buf);
337         serv_gets(serv_sock, buf);
338         snprintf(buf, sizeof buf, "PASS %s", test_pass);
339         serv_puts(serv_sock, buf);
340         serv_gets(serv_sock, buf);
341
342         for (i=0; i<nrooms; ++i) {
343                 snprintf(buf, sizeof buf, "CRE8 1|%s|", random_rooms[i]);
344                 serv_puts(serv_sock, buf);
345                 serv_gets(serv_sock, buf);
346         }
347 }
348
349
350 // Main loop.  Do things and have fun.
351 int main(int argc, char **argv) {
352         int i;
353         int nthreads = 10;
354         int row, col;
355
356         fprintf(stderr, "\033[2J\033[H\033[44m\033[1m"
357                 "╔════════════════════════════════════════════════════════════════════════╗\n"
358                 "║ Load testing utility for Citadel                                       ║\n"
359                 "║ Copyright (c) 2023-2024 by citadel.org et al.                          ║\n"
360                 "║ This program is open source software.  Use, duplication, or disclosure ║\n"
361                 "║ is subject to the terms of the GNU General Public license v3.          ║\n"
362                 "╚════════════════════════════════════════════════════════════════════════╝\033[0m\n"
363         );
364
365         // Parse command line
366         while ((i = getopt(argc, argv, "h:n:")) != EOF) {
367                 switch (i) {
368                 case 'h':
369                         strncpy(ctdldir, optarg, sizeof ctdldir);
370                         break;
371                 case 'n':
372                         nthreads = atoi(optarg);
373                         break;
374                 default:
375                         fprintf(stderr, "loadtest: usage: %s [-h server_dir] [-n number_of_threads]\n", argv[0]);
376                         return(1);
377                 }
378         }
379
380         if (chdir(ctdldir) != 0) {
381                 fprintf(stderr, "loadtest: %s: %s\n", ctdldir, strerror(errno));
382                 exit(errno);
383         }
384
385         // Generate a random password for load test user.  No one needs this password except us.
386         srand(time(NULL)+getpid());
387         for (i=0; i<sizeof(test_pass)-1; ++i) {
388                 test_pass[i] = (rand() % 74) + 48;
389         }
390         test_pass[sizeof(test_pass)] = 0;
391
392         // paint the screen
393         for (i=0; i<nthreads; ++i) {
394                 row = ROW_OFFSET + (i % 20);
395                 col = (i / 20) * 10;
396                 printf("\033[%d;%dH\033[31m       -\033[0m", row, col);
397                 fflush(stdout);
398         }
399
400         // start connecting
401         int serv_sock = uds_connectsock(file_citadel_admin_socket);
402         if (serv_sock < 0) {
403                 fprintf(stderr, "loadtest: cannot connect to Citadel Server\n");
404                 exit(1);
405         }
406
407         char buf[SIZ];
408         serv_gets(serv_sock, buf);
409         setup_accounts(serv_sock);
410         close(serv_sock);
411
412         size_t * threadId = calloc(nthreads, sizeof(size_t));
413         for (size_t i = 0; i < nthreads; ++i) {
414                 threadId[i] = i;
415         }
416
417         time_started = time(NULL);
418         ops_completed = 0;
419
420         for (i=1; i<nthreads; ++i) {
421
422                 pthread_t thread;
423                 pthread_attr_t attr;
424                 int ret = 0;
425
426                 ret = pthread_attr_init(&attr);
427                 ret = pthread_attr_setstacksize(&attr, THREADSTACKSIZE);
428                 ret = pthread_create(&thread, &attr, loadtest, &threadId[i]);
429                 if (ret != 0) {
430                         exit(ret);
431                 }
432
433         }
434         loadtest(&threadId[0]);
435         return(0);
436 }