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.