This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch tymux
in repository terminology.

View the commit online.

commit 115221d623e7277cf0fe975ee4b238920c94bab7
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 22 15:08:57 2026 -0600

    feat: add tybench headless throughput benchmark for tymuxd
    
    Standalone benchmark that spawns a tymuxd session, connects as a
    minimal client, runs `seq 1 N` inside the session, and measures
    wall-clock time until exit.  Reports lines/sec and approximate
    MB/sec.  Uses raw POSIX sockets (no EFL dependency) following the
    same self-contained protocol pattern as the tymux CLI proxy.
---
 src/bin/meson.build |   6 +
 src/bin/tybench.c   | 641 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 647 insertions(+)

diff --git a/src/bin/meson.build b/src/bin/meson.build
index 641088d8..f3387d04 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -169,6 +169,12 @@ if host_os == 'linux'
              install: true,
              include_directories: config_dir,
              dependencies : [])
+
+  executable('tybench',
+             ['tybench.c'],
+             install: false,
+             include_directories: config_dir,
+             dependencies : [])
 endif
 
 if fuzzing
diff --git a/src/bin/tybench.c b/src/bin/tybench.c
new file mode 100644
index 00000000..1aac1a3f
--- /dev/null
+++ b/src/bin/tybench.c
@@ -0,0 +1,641 @@
+/*
+ * tybench — headless tymux throughput benchmark
+ *
+ * Usage: tybench [lines]
+ *   Defaults to 100000 lines if not specified.
+ *
+ * Spawns a tymuxd session, connects as a minimal client, runs
+ * `seq 1 N; exit` inside the session's shell, then measures wall-clock
+ * time from the moment the INPUT is sent until TSRV_MSG_EXIT arrives.
+ * Reports throughput in lines/sec and MB/sec (raw bytes through the PTY).
+ *
+ * This is intentionally free of EFL / Ecore.  Pure POSIX, raw poll().
+ * Protocol types are mirrored inline (same approach as tymux.c).
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "terminology_config.h"
+#endif
+
+#ifndef PACKAGE_BIN_DIR
+# define PACKAGE_BIN_DIR "/usr/local/bin"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <poll.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <sys/mman.h>
+#include <stdint.h>
+
+/* ── path helpers (mirrors termsrv.c / tymux.c, no EFL) ────────────── */
+
+static const char *
+_xdg(void)
+{
+   const char *d = getenv("XDG_RUNTIME_DIR");
+   return (d && *d) ? d : "/tmp";
+}
+
+static int
+_socket_path(const char *name, char *buf, size_t bufsz)
+{
+   if (!name || name[0] == '\0' || strchr(name, '/')) return -1;
+   int n = snprintf(buf, bufsz, "%s/terminology/tymux/%s.sock", _xdg(), name);
+   return (n > 0 && (size_t)n < bufsz) ? 0 : -1;
+}
+
+static int
+_ensure_sessions_dir(void)
+{
+   char buf[512];
+   if (snprintf(buf, sizeof(buf), "%s/terminology", _xdg())
+           >= (int)sizeof(buf)) return -1;
+   if (mkdir(buf, 0700) < 0 && errno != EEXIST) return -1;
+   if (snprintf(buf, sizeof(buf), "%s/terminology/tymux", _xdg())
+           >= (int)sizeof(buf)) return -1;
+   if (mkdir(buf, 0700) < 0 && errno != EEXIST) return -1;
+   return 0;
+}
+
+/* ── inline protocol types (mirrors termsrv.h without EFL deps) ─────── */
+
+typedef enum {
+   _TSRV_MSG_ATTACH   = 1,
+   _TSRV_MSG_ATTACHED = 2,
+   _TSRV_MSG_DETACH   = 3,
+   _TSRV_MSG_INPUT    = 4,
+   _TSRV_MSG_RESIZE   = 5,
+   _TSRV_MSG_NOTIFY   = 6,
+   _TSRV_MSG_TITLE    = 7,
+   _TSRV_MSG_BELL     = 8,
+   _TSRV_MSG_EXIT            = 9,
+   _TSRV_MSG_BACKLOG_BUF_REQ = 10,
+   _TSRV_MSG_BACKLOG_BUF     = 11,
+} _TsrvMsgType;
+
+typedef struct {
+   uint32_t type;
+   uint32_t size;
+} _TsrvMsgHdr;
+
+typedef struct {
+   uint32_t cols;
+   uint32_t rows;
+   uint32_t backlog_buf_count;
+   uint32_t backlog_max_lines;
+   uint32_t oldest_buf_id;
+} _TsrvMsgAttached;
+
+typedef struct {
+   int32_t exit_code;
+} _TsrvMsgExit;
+
+/* ── inline send ─────────────────────────────────────────────────────── */
+
+static int
+_proto_send(int fd, _TsrvMsgType type, const void *payload, uint32_t size)
+{
+   _TsrvMsgHdr hdr = { .type = (uint32_t)type, .size = size };
+   struct iovec iov[2] = {
+      { .iov_base = &hdr,            .iov_len = sizeof(hdr) },
+      { .iov_base = (void *)payload, .iov_len = size        },
+   };
+   int iovcnt = (size && payload) ? 2 : 1;
+   struct msghdr msg = { .msg_iov = iov, .msg_iovlen = (size_t)iovcnt };
+   ssize_t expected = (ssize_t)(sizeof(hdr) + ((payload && size) ? size : 0));
+   ssize_t n = sendmsg(fd, &msg, MSG_NOSIGNAL);
+   return (n == expected) ? 0 : -1;
+}
+
+/* ── inline recv ─────────────────────────────────────────────────────── */
+
+static int
+_recv_exact(int fd, void *buf, size_t need)
+{
+   size_t got = 0;
+   int retries = 0;
+
+   while (got < need)
+     {
+        ssize_t r = recv(fd, (char *)buf + got, need - got, 0);
+        if (r > 0)
+          {
+             got += (size_t)r;
+             retries = 0;
+          }
+        else if (r == 0)
+          {
+             return -1; /* EOF */
+          }
+        else
+          {
+             if (errno == EINTR) continue;
+             if ((errno == EAGAIN || errno == EWOULDBLOCK) && retries < 100)
+               {
+                  retries++;
+                  usleep(1000);
+                  continue;
+               }
+             return -1;
+          }
+     }
+   return 0;
+}
+
+/*
+ * Receive one message.  Returns msg type (>0) on success.
+ * Returns 0 on EAGAIN (no data yet).  Returns -1 on error/EOF.
+ * *payload_out is malloc'd; caller frees.  NULL when payload is empty.
+ * If recv_fd_out is non-NULL and an SCM_RIGHTS fd is present, it is
+ * stored there; otherwise any received fd is closed immediately.
+ */
+static int
+_proto_recv(int fd, void **payload_out, uint32_t *size_out, int *recv_fd_out)
+{
+   _TsrvMsgHdr hdr;
+   *payload_out = NULL;
+   *size_out    = 0;
+   if (recv_fd_out)
+     *recv_fd_out = -1;
+
+   ssize_t r = recv(fd, &hdr, sizeof(hdr), MSG_DONTWAIT);
+   if (r == 0) return -1;
+   if (r < 0)
+     {
+        if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
+        if (errno == EINTR) return 0;
+        return -1;
+     }
+   if (r != (ssize_t)sizeof(hdr))
+     {
+        if (_recv_exact(fd, (char *)&hdr + r, sizeof(hdr) - (size_t)r) < 0)
+          return -1;
+     }
+
+   if (hdr.type == 0 || hdr.type > 11) return -1;
+   if (hdr.size > (1u << 20)) return -1;
+
+   /* Use recvmsg to drain any ancillary fd that may accompany the payload */
+   char cmsgbuf[CMSG_SPACE(sizeof(int))];
+   memset(cmsgbuf, 0, sizeof(cmsgbuf));
+
+   void  *databuf = NULL;
+   size_t total   = (size_t)hdr.size;
+
+   if (total > 0)
+     {
+        databuf = malloc(total);
+        if (!databuf) return -1;
+     }
+
+   struct iovec iov = { .iov_base = databuf ? databuf : (void *)&hdr,
+                        .iov_len  = total > 0 ? total : 0 };
+   struct msghdr msg = {
+      .msg_iov        = &iov,
+      .msg_iovlen     = total > 0 ? 1 : 0,
+      .msg_control    = cmsgbuf,
+      .msg_controllen = sizeof(cmsgbuf),
+   };
+
+   ssize_t n = recvmsg(fd, &msg, total > 0 ? MSG_WAITALL : 0);
+   if (total > 0 && n != (ssize_t)total)
+     {
+        free(databuf);
+        return -1;
+     }
+
+   /* Handle ancillary fd */
+   struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
+   if (cm && cm->cmsg_level == SOL_SOCKET &&
+       cm->cmsg_type  == SCM_RIGHTS &&
+       cm->cmsg_len   >= CMSG_LEN(sizeof(int)))
+     {
+        int rfd;
+        memcpy(&rfd, CMSG_DATA(cm), sizeof(int));
+        if (recv_fd_out)
+          *recv_fd_out = rfd;
+        else
+          close(rfd);
+     }
+
+   *payload_out = databuf;
+   *size_out    = hdr.size;
+   return (int)hdr.type;
+}
+
+/* ── ATTACHED receive (SCM_RIGHTS carries up to 2 fds) ──────────────── */
+
+static int
+_proto_recv_attached(int sock_fd, int *shm_fd_out,
+                     uint32_t *cols_out, uint32_t *rows_out)
+{
+   _TsrvMsgHdr      hdr;
+   _TsrvMsgAttached pl;
+   char cmsgbuf[CMSG_SPACE(2 * sizeof(int))];
+   memset(cmsgbuf, 0, sizeof(cmsgbuf));
+   struct iovec iov[2] = {
+      { &hdr, sizeof(hdr) },
+      { &pl,  sizeof(pl)  }
+   };
+   struct msghdr msg = {
+      .msg_iov        = iov,
+      .msg_iovlen     = 2,
+      .msg_control    = cmsgbuf,
+      .msg_controllen = sizeof(cmsgbuf)
+   };
+
+   *shm_fd_out = -1;
+   ssize_t n = recvmsg(sock_fd, &msg, MSG_WAITALL);
+   if (n < (ssize_t)(sizeof(hdr) + sizeof(pl))) return -1;
+   if (msg.msg_flags & MSG_CTRUNC) return -1;
+   if (hdr.type != _TSRV_MSG_ATTACHED) return -1;
+   if (hdr.size != sizeof(pl)) return -1;
+
+   struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
+   if (!cm || cm->cmsg_type != SCM_RIGHTS ||
+       cm->cmsg_len < CMSG_LEN(sizeof(int))) return -1;
+   memcpy(shm_fd_out, CMSG_DATA(cm), sizeof(int));
+
+   /* Close the optional backlog fd — benchmark does not use it */
+   if (cm->cmsg_len >= CMSG_LEN(2 * sizeof(int)))
+     {
+        int backlog_fd = -1;
+        memcpy(&backlog_fd, (char *)CMSG_DATA(cm) + sizeof(int), sizeof(int));
+        if (backlog_fd >= 0)
+          close(backlog_fd);
+     }
+
+   *cols_out = pl.cols;
+   *rows_out = pl.rows;
+   return 0;
+}
+
+/* ── timing helper ───────────────────────────────────────────────────── */
+
+static double
+_now(void)
+{
+   struct timespec ts;
+   clock_gettime(CLOCK_MONOTONIC, &ts);
+   return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
+}
+
+/* ── daemon spawn + wait for socket ─────────────────────────────────── */
+
+static int
+_spawn_daemon(const char *session_name, const char *sock_path)
+{
+   pid_t child = fork();
+   if (child < 0)
+     {
+        fprintf(stderr, "tybench: fork: %s\n", strerror(errno));
+        return -1;
+     }
+
+   if (child == 0)
+     {
+        /* Double-fork: daemon is reparented to init, no zombie */
+        pid_t gc = fork();
+        if (gc < 0) _exit(1);
+        if (gc > 0) _exit(0); /* first child exits; grandchild continues */
+
+        setsid();
+
+        int devnull = open("/dev/null", O_RDWR);
+        if (devnull >= 0)
+          {
+             dup2(devnull, STDIN_FILENO);
+             dup2(devnull, STDOUT_FILENO);
+             dup2(devnull, STDERR_FILENO);
+             if (devnull > STDERR_FILENO)
+               close(devnull);
+          }
+
+        /* Try build directory first (for development), then install path,
+         * then PATH fallback. */
+        {
+           /* Derive build dir from our own /proc/self/exe path:
+            * .../build/src/bin/tybench → .../build/src/bin/tymuxd */
+           char self[4096], build_tymuxd[4096 + 16];
+           ssize_t slen = readlink("/proc/self/exe", self, sizeof(self) - 1);
+           if (slen > 0)
+             {
+                self[slen] = '\0';
+                char *slash = strrchr(self, '/');
+                if (slash)
+                  {
+                     slash[1] = '\0';
+                     snprintf(build_tymuxd, sizeof(build_tymuxd),
+                              "%stymuxd", self);
+                     execl(build_tymuxd, "tymuxd", session_name, (char *)NULL);
+                  }
+             }
+        }
+        execl(PACKAGE_BIN_DIR "/tymuxd",
+              "tymuxd", session_name, (char *)NULL);
+        execlp("tymuxd", "tymuxd", session_name, (char *)NULL);
+        _exit(127);
+     }
+
+   /* Reap the short-lived first child */
+   waitpid(child, NULL, 0);
+
+   /* Poll with connect probes until daemon is listening (up to 3 s) */
+   struct timespec ts_sleep = { .tv_sec = 0, .tv_nsec = 10 * 1000 * 1000 };
+   for (int i = 0; i < 300; i++)
+     {
+        nanosleep(&ts_sleep, NULL);
+
+        int probe = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+        if (probe < 0) continue;
+
+        struct sockaddr_un sa;
+        memset(&sa, 0, sizeof(sa));
+        sa.sun_family = AF_UNIX;
+        strncpy(sa.sun_path, sock_path, sizeof(sa.sun_path) - 1);
+
+        if (connect(probe, (struct sockaddr *)&sa, sizeof(sa)) == 0)
+          {
+             close(probe);
+             return 0;
+          }
+        close(probe);
+     }
+
+   fprintf(stderr, "tybench: daemon did not start within 3 s: %s\n",
+           sock_path);
+   return -1;
+}
+
+/* ── connect to running daemon ───────────────────────────────────────── */
+
+static int
+_connect(const char *sock_path)
+{
+   int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+   if (fd < 0)
+     {
+        fprintf(stderr, "tybench: socket: %s\n", strerror(errno));
+        return -1;
+     }
+
+   struct sockaddr_un sa;
+   memset(&sa, 0, sizeof(sa));
+   sa.sun_family = AF_UNIX;
+   strncpy(sa.sun_path, sock_path, sizeof(sa.sun_path) - 1);
+
+   if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
+     {
+        fprintf(stderr, "tybench: connect %s: %s\n",
+                sock_path, strerror(errno));
+        close(fd);
+        return -1;
+     }
+   return fd;
+}
+
+/* ── main ─────────────────────────────────────────────────────────────── */
+
+int
+main(int argc, char *argv[])
+{
+   long lines = 100000;
+   if (argc >= 2)
+     {
+        char *end;
+        lines = strtol(argv[1], &end, 10);
+        if (*end != '\0' || lines <= 0)
+          {
+             fprintf(stderr, "tybench: invalid line count '%s'\n", argv[1]);
+             return 1;
+          }
+     }
+
+   /* Ignore SIGPIPE — we handle write errors via return values */
+   signal(SIGPIPE, SIG_IGN);
+
+   /* Build unique session name */
+   char session_name[64];
+   snprintf(session_name, sizeof(session_name), "tybench-%d", (int)getpid());
+
+   char sock_path[512];
+   if (_socket_path(session_name, sock_path, sizeof(sock_path)) < 0)
+     {
+        fprintf(stderr, "tybench: session name too long\n");
+        return 1;
+     }
+
+   /* Ensure the socket directory exists */
+   if (_ensure_sessions_dir() < 0)
+     {
+        fprintf(stderr, "tybench: cannot create sessions dir: %s\n",
+                strerror(errno));
+        return 1;
+     }
+
+   printf("tybench: session '%s'\n", session_name);
+   printf("tybench: spawning tymuxd...\n");
+
+   if (_spawn_daemon(session_name, sock_path) < 0)
+     return 1;
+
+   printf("tybench: daemon ready, connecting...\n");
+
+   int sock_fd = _connect(sock_path);
+   if (sock_fd < 0)
+     return 1;
+
+   /* Send ATTACH (no payload) */
+   if (_proto_send(sock_fd, _TSRV_MSG_ATTACH, NULL, 0) < 0)
+     {
+        fprintf(stderr, "tybench: send ATTACH failed: %s\n", strerror(errno));
+        close(sock_fd);
+        return 1;
+     }
+
+   /* Receive ATTACHED + shm fd */
+   int      shm_fd = -1;
+   uint32_t cols   = 0;
+   uint32_t rows   = 0;
+   if (_proto_recv_attached(sock_fd, &shm_fd, &cols, &rows) < 0)
+     {
+        fprintf(stderr, "tybench: recv ATTACHED failed\n");
+        close(sock_fd);
+        return 1;
+     }
+
+   printf("tybench: attached (cols=%u rows=%u shm_fd=%d)\n",
+          cols, rows, shm_fd);
+
+   /* mmap the shm read-only (we only hold it to match real client behaviour;
+    * the benchmark does not read cell data) */
+   size_t shm_size = 4096u + (size_t)500 * 200 * 8u; /* TERMSRV_SHM_TOTAL_SIZE */
+   void  *shm      = MAP_FAILED;
+   if (shm_fd >= 0)
+     {
+        shm = mmap(NULL, shm_size, PROT_READ, MAP_SHARED, shm_fd, 0);
+        if (shm == MAP_FAILED)
+          fprintf(stderr, "tybench: mmap shm failed (non-fatal): %s\n",
+                  strerror(errno));
+        close(shm_fd);
+        shm_fd = -1;
+     }
+
+   /* Build the shell command: seq 1 <lines>; exit */
+   char cmd[128];
+   int  cmd_len = snprintf(cmd, sizeof(cmd), "seq 1 %ld; exit\n", lines);
+   if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd))
+     {
+        fprintf(stderr, "tybench: command too long\n");
+        close(sock_fd);
+        return 1;
+     }
+
+   /* Estimated output size: "N\n" per line, average ~5 chars for 100 k */
+   printf("tybench: running: seq 1 %ld; exit\n", lines);
+
+   /* ── SEND INPUT + start timer ────────────────────────────────────── */
+   double t_start = _now();
+
+   if (_proto_send(sock_fd, _TSRV_MSG_INPUT,
+                   cmd, (uint32_t)cmd_len) < 0)
+     {
+        fprintf(stderr, "tybench: send INPUT failed: %s\n", strerror(errno));
+        close(sock_fd);
+        return 1;
+     }
+
+   /* ── MESSAGE LOOP ─────────────────────────────────────────────────── */
+   long   notify_count = 0;
+   long   title_count  = 0;
+   int    exit_code    = 0;
+   int    done         = 0;
+
+   struct pollfd pfd = { .fd = sock_fd, .events = POLLIN };
+
+   while (!done)
+     {
+        int pr = poll(&pfd, 1, 10000 /* 10 s timeout */);
+        if (pr < 0)
+          {
+             if (errno == EINTR) continue;
+             fprintf(stderr, "tybench: poll: %s\n", strerror(errno));
+             break;
+          }
+        if (pr == 0)
+          {
+             fprintf(stderr, "tybench: timeout waiting for daemon\n");
+             break;
+          }
+
+        if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
+          {
+             fprintf(stderr, "tybench: socket closed unexpectedly\n");
+             break;
+          }
+
+        if (!(pfd.revents & POLLIN))
+          continue;
+
+        /* Drain all available messages before polling again */
+        for (;;)
+          {
+             void    *payload  = NULL;
+             uint32_t pay_size = 0;
+             int      anc_fd   = -1;
+
+             int type = _proto_recv(sock_fd, &payload, &pay_size, &anc_fd);
+             if (type == 0) break;  /* EAGAIN — back to poll */
+             if (type < 0)
+               {
+                  done = 1;
+                  break;
+               }
+
+             /* Close any unexpected ancillary fd to prevent leaks */
+             if (anc_fd >= 0)
+               {
+                  close(anc_fd);
+                  anc_fd = -1;
+               }
+
+             switch ((_TsrvMsgType)type)
+               {
+                case _TSRV_MSG_NOTIFY:
+                   notify_count++;
+                   break;
+
+                case _TSRV_MSG_TITLE:
+                   title_count++;
+                   break;
+
+                case _TSRV_MSG_BELL:
+                   break;
+
+                case _TSRV_MSG_EXIT:
+                   if (payload && pay_size >= sizeof(_TsrvMsgExit))
+                     exit_code = (int)(((_TsrvMsgExit *)payload)->exit_code);
+                   done = 1;
+                   break;
+
+                default:
+                   /* BACKLOG_BUF, etc. — ignore */
+                   break;
+               }
+
+             free(payload);
+
+             if (done) break;
+          }
+     }
+
+   double t_end     = _now();
+   double elapsed   = t_end - t_start;
+
+   /* ── RESULTS ─────────────────────────────────────────────────────── */
+   printf("\n");
+   printf("tybench: --- results ---\n");
+   printf("tybench: lines          : %ld\n", lines);
+   printf("tybench: elapsed        : %.3f s\n", elapsed);
+   if (elapsed > 0.0)
+     {
+        double lines_per_sec = (double)lines / elapsed;
+        /* Average output size per line: digits + newline.
+         * log10(lines)+1 digits + 1 newline = rough estimate. */
+        double avg_line_bytes = 1.0;
+        {
+           long tmp = lines;
+           while (tmp > 0) { avg_line_bytes += 1.0; tmp /= 10; }
+        }
+        double mbytes = ((double)lines * avg_line_bytes) / (1024.0 * 1024.0);
+        double mb_per_sec = mbytes / elapsed;
+        printf("tybench: lines/sec      : %.0f\n", lines_per_sec);
+        printf("tybench: ~MB/sec (PTY)  : %.2f\n", mb_per_sec);
+     }
+   printf("tybench: NOTIFY msgs    : %ld\n", notify_count);
+   printf("tybench: TITLE msgs     : %ld\n", title_count);
+   printf("tybench: exit code      : %d\n", exit_code);
+
+   /* ── CLEANUP ─────────────────────────────────────────────────────── */
+   if (shm != MAP_FAILED)
+     munmap(shm, shm_size);
+
+   /* Send DETACH so the daemon exits cleanly */
+   _proto_send(sock_fd, _TSRV_MSG_DETACH, NULL, 0);
+   close(sock_fd);
+
+   /* Remove the socket file */
+   unlink(sock_path);
+
+   return (exit_code == 0) ? 0 : 1;
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to