Added context state to output of RWHO command.
[citadel.git] / citadel / utils / loadtest.c
index bb713ea3da4a9a94e855ff1db61f5c5929cb3718..8d4e41b2f6e26708edabeffa7a3c4f65bf6055ae 100644 (file)
@@ -1,6 +1,6 @@
-// unfinished load testing utility for Citadel Server
+// Load testing utility for Citadel Server
 //
-// Copyright (c) 1987-2023 by the citadel.org team
+// Copyright (c) 1987-2024 by the citadel.org team
 //
 // This program is open source software.  Use, duplication, or disclosure
 // is subject to the terms of the GNU General Public License, version 3.
 #include <limits.h>
 #include <sys/socket.h>
 #include <sys/un.h>
+#include <pthread.h>
 #include "../server/citadel_defs.h"
 #include "../server/server.h"
 #include "../server/citadel_dirs.h"
 #include <libcitadel.h>
 
+char ctdldir[PATH_MAX]=CTDLDIR;
+
 char *words[] = {
        "lorem","ipsum","dolor","sit","amet","consectetuer","adipiscing","elit","integer","in","mi","a","mauris",
        "ornare","sagittis","suspendisse","potenti","suspendisse","dapibus","dignissim","dolor","nam",
@@ -86,7 +89,7 @@ char *words[] = {
        "tempor","tincidunt","phasellus","justo","purus","pharetra","ut","ultricies","nec","consequat","vel",
        "nisi","fusce","vitae","velit","at","libero","sollicitudin","sodales","aenean","mi","libero","ultrices",
        "id","suscipit","vitae","dapibus","eu","metus","aenean","vestibulum","nibh","ac","massa","vivamus",
-       "vestibulum","libero","vitae","purus","in","hac","habitasse","platea","dictumst","curabitur",
+       "vestibulum","libero","vitae","purus","in","hac","maga","habitasse","platea","dictumst","curabitur",
        "blandit","nunc","non","arcu","ut","nec","nibh","morbi","quis","leo","vel","magna","commodo","rhoncus",
        "donec","congue","leo","eu","lacus","pellentesque","at","erat","id","mi","consequat","congue","praesent",
        "a","nisl","ut","diam","interdum","molestie","fusce","suscipit","rhoncus","sem","donec","pretium",
@@ -113,7 +116,6 @@ char *words[] = {
 };
 int nwords = sizeof(words) / sizeof(char *);
 
-int serv_sock = (-1);
 
 int uds_connectsock(char *sockpath) {
        int s;
@@ -125,22 +127,20 @@ int uds_connectsock(char *sockpath) {
 
        s = socket(AF_UNIX, SOCK_STREAM, 0);
        if (s < 0) {
-               fprintf(stderr, "sendcommand: Can't create socket: %s\n", strerror(errno));
-               exit(3);
+               return(-1);
        }
 
        if (connect(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
-               fprintf(stderr, "sendcommand: can't connect: %s\n", strerror(errno));
                close(s);
-               exit(3);
+               return(-1);
        }
 
-       return s;
+       return(s);
 }
 
 
 // input binary data from socket
-void serv_read(char *buf, int bytes) {
+void serv_read(int serv_sock, char *buf, int bytes) {
        int len, rlen;
 
        len = 0;
@@ -155,7 +155,7 @@ void serv_read(char *buf, int bytes) {
 
 
 // send binary to server
-void serv_write(char *buf, int nbytes) {
+void serv_write(int serv_sock, char *buf, int nbytes) {
        int bytes_written = 0;
        int retval;
        while (bytes_written < nbytes) {
@@ -169,12 +169,12 @@ void serv_write(char *buf, int nbytes) {
 
 
 // input string from socket - implemented in terms of serv_read()
-void serv_gets(char *buf) {
+void serv_gets(int serv_sock, char *buf) {
        int i;
 
        // Read one character at a time.
        for (i = 0;; i++) {
-               serv_read(&buf[i], 1);
+               serv_read(serv_sock, &buf[i], 1);
                if (buf[i] == '\n' || i == (SIZ-1))
                        break;
        }
@@ -182,7 +182,7 @@ void serv_gets(char *buf) {
        // If we got a long line, discard characters until the newline.
        if (i == (SIZ-1)) {
                while (buf[i] != '\n') {
-                       serv_read(&buf[i], 1);
+                       serv_read(serv_sock, &buf[i], 1);
                }
        }
 
@@ -192,13 +192,12 @@ void serv_gets(char *buf) {
 
 
 // send line to server - implemented in terms of serv_write()
-void serv_puts(char *buf) {
-       serv_write(buf, strlen(buf));
-       serv_write("\n", 1);
+void serv_puts(int serv_sock, char *buf) {
+       serv_write(serv_sock, buf, strlen(buf));
+       serv_write(serv_sock, "\n", 1);
 }
 
 
-
 char *random_rooms[] = {
        "Load Testing Test Room One",
        "Load Test 2: Electric Boogaloo",
@@ -207,10 +206,14 @@ char *random_rooms[] = {
        "Five Guys Load Testing and Fries"
 };
 int nrooms = sizeof(random_rooms) / sizeof(char *);
-
 char *test_user = "Load Test User";
+char test_pass[16];
+time_t time_started;
+volatile int ops_completed;
 
-void perform_random_thing(void) {
+
+// These are our randomized load test operations: an even mix of changing rooms, posting messages, and deleting messages.
+void perform_random_thing(int serv_sock) {
        int op = random() % 3;
        char buf[SIZ];
        int i;
@@ -219,14 +222,14 @@ void perform_random_thing(void) {
        // Random operation 0 : change rooms
        if (op == 0) {
                snprintf(buf, sizeof(buf), "GOTO %s", random_rooms[random() % nrooms]);
-               serv_puts(buf);
-               serv_gets(buf);
+               serv_puts(serv_sock, buf);
+               serv_gets(serv_sock, buf);
        }
 
        // Random operation 1 : post a message
        if (op == 1) {
-               serv_puts("ENT0 1");
-               serv_gets(buf);
+               serv_puts(serv_sock, "ENT0 1");
+               serv_gets(serv_sock, buf);
                if (buf[0] == '4') {
 
                        bigness = random() % 500;
@@ -234,15 +237,15 @@ void perform_random_thing(void) {
                        for (i=0; i<bigness; ++i) {
                                strcat(buf, words[random() % nwords]);
                                if ( (i != 0) && ((i % 10) == 0) ) {
-                                       serv_puts(buf);
+                                       serv_puts(serv_sock, buf);
                                        strcpy(buf, "");
                                }
                                else {
                                        strcat(buf, " ");
                                }
                        }
-                       serv_puts(buf);
-                       serv_puts("000");
+                       serv_puts(serv_sock, buf);
+                       serv_puts(serv_sock, "000");
                }
        }
 
@@ -253,102 +256,181 @@ void perform_random_thing(void) {
                total_msgs = 0;
                selected_msg = 0;
 
-               serv_puts("MSGS ALL");
-               serv_gets(buf);
-               if (buf[0] == '1') {
-                       while (serv_gets(buf), strcmp(buf, "000")) {
-                               ++total_msgs;
-                               if ((random() % total_msgs) == 0) {
-                                       selected_msg = atol(buf);
+               do {
+                       serv_puts(serv_sock, "MSGS ALL");
+                       serv_gets(serv_sock, buf);
+                       if (buf[0] == '1') {
+                               while (serv_gets(serv_sock, buf), strcmp(buf, "000")) {
+                                       ++total_msgs;
+                                       if ((random() % total_msgs) == 0) {
+                                               selected_msg = atol(buf);
+                                       }
                                }
                        }
-               }
-               snprintf(buf, sizeof buf, "DELE %ld", selected_msg);
-               serv_puts(buf);
-               serv_gets(buf);
+                       snprintf(buf, sizeof buf, "DELE %ld", selected_msg);
+                       serv_puts(serv_sock, buf);
+                       serv_gets(serv_sock, buf);
+               } while ( (buf[0] != '2') && (total_msgs > 0));
        }
-
 }
 
+#define ROW_OFFSET 8
 
-void do_stuff(void) {
-       int i;
+// This is the main loop.  We log in as the load test user, and then perform random operations until stopped.
+void *loadtest(void *pointer_to_thread_id) {
        char buf[SIZ];
+       int serv_sock;
 
-       printf("Creating test user '%s'\n", test_user);
-       snprintf(buf, sizeof buf, "CREU %s", test_user);
-       serv_puts(buf);
-       serv_gets(buf);
-       printf("%s\n", buf);
-       snprintf(buf, sizeof buf, "ASUP %s|640k_enough_ne1|0|||6|", test_user);
-       serv_puts(buf);
-       serv_gets(buf);
-       printf("%s\n", buf);
-       snprintf(buf, sizeof buf, "USER %s", test_user);
-       serv_puts(buf);
-       serv_gets(buf);
-       printf("%s\n", buf);
-       snprintf(buf, sizeof buf, "PASS 640k_enough_ne1");
-       serv_puts(buf);
-       serv_gets(buf);
-       printf("%s\n", buf);
+       int thread_id = *(int *)pointer_to_thread_id;
 
-       for (i=0; i<nrooms; ++i) {
-               printf("Creating test room '%s'\n", random_rooms[i]);
-               snprintf(buf, sizeof buf, "CRE8 1|%s|", random_rooms[i]);
-               serv_puts(buf);
-               serv_gets(buf);
-               printf("%s\n", buf);
+       serv_sock = uds_connectsock(file_citadel_socket);
+
+       if (serv_sock < 0) {
+               printf("\033[8;0H\033[31mWarning: some threads failed to connect to Citadel Server.\033[0m");
+               fflush(stdout);
+               pthread_exit(NULL);
        }
 
+       serv_gets(serv_sock, buf);
+       snprintf(buf, sizeof buf, "USER %s", test_user);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
+       snprintf(buf, sizeof buf, "PASS %s", test_pass);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
        snprintf(buf, sizeof(buf), "GOTO %s", random_rooms[0]);
-       serv_puts(buf);
-       serv_gets(buf);
-       printf("%s\n", buf);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
+
+       // Find a nice spot on the screen to show the operation count for this thread.
+       int row = ROW_OFFSET + (thread_id % 20);
+       int col = (thread_id / 20) * 10;
+       long ops = 0;
+       printf("\033[%d;%dH\033[33m       0\033[0m", row, col);
+       fflush(stdout);
 
        while(1) {
-               perform_random_thing();
+               perform_random_thing(serv_sock);
+               printf("\033[%d;%dH\033[32m%8ld\033[0m", row, col, ++ops);
+               ++ops_completed;                // this is declared "volatile" so we don't need to lock it
+               if (thread_id == 0) {
+                       printf("\033[2;55H\033[44m\033[33m\033[1m%ld ops/sec \033[0m", (ops_completed / (time(NULL) - time_started)));
+               }
+               fflush(stdout);
        }
 }
 
 
+// Create (or replace) the account used for load testing, then create the rooms in which we will load test.
+void setup_accounts(int serv_sock) {
+       int i;
+       char buf[SIZ];
+
+       snprintf(buf, sizeof buf, "CREU %s", test_user);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
+       snprintf(buf, sizeof buf, "ASUP %s|%s|0|||6|", test_user, test_pass);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
+       snprintf(buf, sizeof buf, "USER %s", test_user);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
+       snprintf(buf, sizeof buf, "PASS %s", test_pass);
+       serv_puts(serv_sock, buf);
+       serv_gets(serv_sock, buf);
+
+       for (i=0; i<nrooms; ++i) {
+               snprintf(buf, sizeof buf, "CRE8 1|%s|", random_rooms[i]);
+               serv_puts(serv_sock, buf);
+               serv_gets(serv_sock, buf);
+       }
+}
+
 
 // Main loop.  Do things and have fun.
 int main(int argc, char **argv) {
-       int a;
-       char buf[SIZ];
-       char ctdldir[PATH_MAX]=CTDLDIR;
+       int i;
+       int nthreads = 10;
+       int row, col;
+
+       fprintf(stderr, "\033[2J\033[H\033[44m\033[1m"
+               "╔════════════════════════════════════════════════════════════════════════╗\n"
+               "║ Load testing utility for Citadel                                       ║\n"
+               "║ Copyright (c) 2023-2024 by citadel.org et al.                          ║\n"
+               "║ This program is open source software.  Use, duplication, or disclosure ║\n"
+               "║ is subject to the terms of the GNU General Public license v3.          ║\n"
+               "╚════════════════════════════════════════════════════════════════════════╝\033[0m\n"
+       );
 
        // Parse command line
-       while ((a = getopt(argc, argv, "h:")) != EOF) {
-               switch (a) {
+       while ((i = getopt(argc, argv, "h:n:")) != EOF) {
+               switch (i) {
                case 'h':
                        strncpy(ctdldir, optarg, sizeof ctdldir);
                        break;
+               case 'n':
+                       nthreads = atoi(optarg);
+                       break;
                default:
-                       fprintf(stderr, "sendcommand: usage: %s [-h server_dir]\n", argv[0]);
+                       fprintf(stderr, "loadtest: usage: %s [-h server_dir] [-n number_of_threads]\n", argv[0]);
                        return(1);
                }
        }
 
-       fprintf(stderr, "sendcommand: started (pid=%d) connecting to Citadel server with data directory %s\n",
-               (int) getpid(),
-               ctdldir
-       );
-       fflush(stderr);
-
        if (chdir(ctdldir) != 0) {
-               fprintf(stderr, "sendcommand: %s: %s\n", ctdldir, strerror(errno));
+               fprintf(stderr, "loadtest: %s: %s\n", ctdldir, strerror(errno));
                exit(errno);
        }
 
-       serv_sock = uds_connectsock(file_citadel_admin_socket);
-       serv_gets(buf);
-       fprintf(stderr, "%s\n", buf);
+       // Generate a random password for load test user.  No one needs this password except us.
+       srand(time(NULL)+getpid());
+       for (i=0; i<sizeof(test_pass)-1; ++i) {
+               test_pass[i] = (rand() % 74) + 48;
+       }
+       test_pass[sizeof(test_pass)] = 0;
+
+       // paint the screen
+       for (i=0; i<nthreads; ++i) {
+               row = ROW_OFFSET + (i % 20);
+               col = (i / 20) * 10;
+               printf("\033[%d;%dH\033[31m       -\033[0m", row, col);
+               fflush(stdout);
+       }
 
-       do_stuff();
+       // start connecting
+       int serv_sock = uds_connectsock(file_citadel_admin_socket);
+       if (serv_sock < 0) {
+               fprintf(stderr, "loadtest: cannot connect to Citadel Server\n");
+               exit(1);
+       }
 
+       char buf[SIZ];
+       serv_gets(serv_sock, buf);
+       setup_accounts(serv_sock);
        close(serv_sock);
 
+       size_t * threadId = calloc(nthreads, sizeof(size_t));
+       for (size_t i = 0; i < nthreads; ++i) {
+               threadId[i] = i;
+       }
+
+       time_started = time(NULL);
+       ops_completed = 0;
+
+       for (i=1; i<nthreads; ++i) {
+
+               pthread_t thread;
+               pthread_attr_t attr;
+               int ret = 0;
+
+               ret = pthread_attr_init(&attr);
+               ret = pthread_attr_setstacksize(&attr, THREADSTACKSIZE);
+               ret = pthread_create(&thread, &attr, loadtest, &threadId[i]);
+               if (ret != 0) {
+                       exit(ret);
+               }
+
+       }
+       loadtest(&threadId[0]);
        return(0);
 }