#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#define HIST_BUCKETS 32
static size_t g_msgsize = 16 * 4096;
static int g_duration = 3;
static int g_pipe_size = 1024 * 1024;
static int g_memory_pressure;
static atomic_int g_stop;
static int g_pipe[2];
struct wstats {
uint64_t writes;
uint64_t bytes;
uint64_t lat_sum_ns;
uint64_t lat_max_ns;
uint64_t lat_hist[HIST_BUCKETS];
char *buf;
};
struct rstats {
char *buf;
};
struct hist_totals {
uint64_t writes;
uint64_t bytes;
uint64_t lat_sum;
uint64_t lat_max;
};
static inline uint64_t now_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
static inline int log2_bucket(uint64_t v)
{
int b = 0;
if (!v)
return 0;
while (v >>= 1)
b++;
return b < HIST_BUCKETS ? b : HIST_BUCKETS - 1;
}
static void *writer(void *arg)
{
struct wstats *s = arg;
while (!atomic_load_explicit(&g_stop, memory_order_relaxed)) {
uint64_t t0 = now_ns();
ssize_t n = write(g_pipe[1], s->buf, g_msgsize);
uint64_t dt = now_ns() - t0;
if (n > 0) {
s->writes++;
s->bytes += (uint64_t)n;
s->lat_sum_ns += dt;
if (dt > s->lat_max_ns)
s->lat_max_ns = dt;
s->lat_hist[log2_bucket(dt)]++;
} else if (n < 0 && (errno == EPIPE || errno == EBADF)) {
break;
}
}
return NULL;
}
static void *reader(void *arg)
{
struct rstats *s = arg;
for (;;) {
ssize_t n = read(g_pipe[0], s->buf, g_msgsize);
if (n <= 0)
break;
}
return NULL;
}
static void aggregate_wstats(struct wstats *all, int nw,
uint64_t agg[HIST_BUCKETS],
struct hist_totals *t)
{
memset(t, 0, sizeof(*t));
for (int i = 0; i < nw; i++) {
t->writes += all[i].writes;
t->bytes += all[i].bytes;
t->lat_sum += all[i].lat_sum_ns;
if (all[i].lat_max_ns > t->lat_max)
t->lat_max = all[i].lat_max_ns;
for (int b = 0; b < HIST_BUCKETS; b++)
agg[b] += all[i].lat_hist[b];
}
}
static uint64_t bucket_at(const uint64_t agg[HIST_BUCKETS], uint64_t target)
{
uint64_t cum = 0;
for (int b = 0; b < HIST_BUCKETS; b++) {
uint64_t upper = (1ULL << (b + 1)) - 1;
cum += agg[b];
if (cum >= target)
return upper;
}
return 0;
}
static void compute_p50_p99(const uint64_t agg[HIST_BUCKETS], uint64_t writes,
uint64_t *p50, uint64_t *p99)
{
uint64_t p50_target, p99_target;
*p50 = *p99 = 0;
if (!writes)
return;
p50_target = writes * 50 / 100;
p99_target = writes * 99 / 100;
if (!p50_target)
p50_target = 1;
if (!p99_target)
p99_target = 1;
*p50 = bucket_at(agg, p50_target);
*p99 = bucket_at(agg, p99_target);
}
static void print_summary(int nw, int nr, const struct hist_totals *t,
uint64_t p50, uint64_t p99)
{
double sec = g_duration;
uint64_t avg_ns = t->writes ? t->lat_sum / t->writes : 0;
printf("config: writers=%d readers=%d msgsize=%zu duration=%d pipe_size=%d memory_pressure=%s\n",
nw, nr, g_msgsize, g_duration, g_pipe_size,
g_memory_pressure ? "yes" : "no");
printf("writes: total=%llu rate=%.0f/s\n",
(unsigned long long)t->writes, (double)t->writes / sec);
printf("throughput_MBps: %.2f\n",
((double)t->bytes / sec) / (1024.0 * 1024.0));
printf("lat_avg_ns: %llu\n", (unsigned long long)avg_ns);
printf("lat_p50_ns_upper: %llu\n", (unsigned long long)p50);
printf("lat_p99_ns_upper: %llu\n", (unsigned long long)p99);
printf("lat_max_ns: %llu\n", (unsigned long long)t->lat_max);
}
static void summarize(struct wstats *all, int nw, int nr)
{
uint64_t agg[HIST_BUCKETS] = {0};
struct hist_totals t;
uint64_t p50, p99;
aggregate_wstats(all, nw, agg, &t);
compute_p50_p99(agg, t.writes, &p50, &p99);
print_summary(nw, nr, &t, p50, p99);
}
static void stress_ng_child(int hs_wr) __attribute__((noreturn));
static void stress_ng_child(int hs_wr)
{
char errbuf[256];
signal(SIGPIPE, SIG_DFL);
execlp("stress-ng", "stress-ng",
"--vm", "4", "--vm-bytes", "80%",
"--vm-method", "all",
(char *)NULL);
snprintf(errbuf, sizeof(errbuf),
"exec stress-ng failed: %s\n", strerror(errno));
(void)!write(hs_wr, errbuf, strlen(errbuf));
_exit(127);
}
static int stress_ng_wait_handshake(int hs_rd, pid_t pid)
{
struct pollfd pfd = { .fd = hs_rd, .events = POLLIN };
char errbuf[256];
int status;
int ret;
ret = poll(&pfd, 1, 500);
if (ret <= 0)
return 0;
ssize_t n = read(hs_rd, errbuf, sizeof(errbuf) - 1);
if (n > 0) {
errbuf[n] = '\0';
fputs(errbuf, stderr);
waitpid(pid, &status, 0);
return -1;
}
return 0;
}
static pid_t spawn_stress_ng(void)
{
int hs[2];
pid_t pid;
if (pipe2(hs, O_CLOEXEC) < 0) {
perror("pipe2");
return -1;
}
pid = fork();
if (pid < 0) {
perror("fork");
close(hs[0]);
close(hs[1]);
return -1;
}
if (pid == 0) {
close(hs[0]);
stress_ng_child(hs[1]);
}
close(hs[1]);
if (stress_ng_wait_handshake(hs[0], pid) < 0) {
close(hs[0]);
return -1;
}
close(hs[0]);
sleep(1);
return pid;
}
static void kill_stress_ng(pid_t pid)
{
int status;
if (pid <= 0)
return;
kill(pid, SIGTERM);
for (int i = 0; i < 20; i++) {
if (waitpid(pid, &status, WNOHANG) > 0)
return;
usleep(100 * 1000);
}
kill(pid, SIGKILL);
waitpid(pid, &status, 0);
}
static int alloc_thread_bufs(struct wstats *ws, int nw,
struct rstats *rs, int nr)
{
for (int i = 0; i < nw; i++) {
ws[i].buf = aligned_alloc(4096, g_msgsize);
if (!ws[i].buf) {
fprintf(stderr, "writer %d: aligned_alloc(%zu) failed\n",
i, g_msgsize);
return -1;
}
memset(ws[i].buf, 0xAA, g_msgsize);
}
for (int i = 0; i < nr; i++) {
rs[i].buf = aligned_alloc(4096, g_msgsize);
if (!rs[i].buf) {
fprintf(stderr, "reader %d: aligned_alloc(%zu) failed\n",
i, g_msgsize);
return -1;
}
}
return 0;
}
static void free_thread_bufs(struct wstats *ws, int nw,
struct rstats *rs, int nr)
{
if (ws)
for (int i = 0; i < nw; i++)
free(ws[i].buf);
if (rs)
for (int i = 0; i < nr; i++)
free(rs[i].buf);
}
static int start_readers(pthread_t *rt, struct rstats *rs, int nr,
int *created)
{
for (int i = 0; i < nr; i++) {
int err = pthread_create(&rt[i], NULL, reader, &rs[i]);
if (err) {
fprintf(stderr, "pthread_create reader %d: %s\n",
i, strerror(err));
return -1;
}
(*created)++;
}
return 0;
}
static int start_writers(pthread_t *wt, struct wstats *ws, int nw,
int *created)
{
for (int i = 0; i < nw; i++) {
int err = pthread_create(&wt[i], NULL, writer, &ws[i]);
if (err) {
fprintf(stderr, "pthread_create writer %d: %s\n",
i, strerror(err));
return -1;
}
(*created)++;
}
return 0;
}
static int open_bench_pipe(void)
{
if (pipe(g_pipe) < 0) {
perror("pipe");
return -1;
}
if (fcntl(g_pipe[1], F_SETPIPE_SZ, g_pipe_size) < 0)
perror("F_SETPIPE_SZ (continuing)");
return 0;
}
static void stop_and_join(pthread_t *wt, int nw_created,
pthread_t *rt, int nr_created, int rc)
{
atomic_store(&g_stop, 1);
close(g_pipe[1]);
if (rc < 0)
close(g_pipe[0]);
for (int i = 0; i < nw_created; i++)
pthread_join(wt[i], NULL);
for (int i = 0; i < nr_created; i++)
pthread_join(rt[i], NULL);
if (rc == 0)
close(g_pipe[0]);
}
static int run_one(int nw, int nr)
{
pthread_t *wt = NULL, *rt = NULL;
struct wstats *ws = NULL;
struct rstats *rs = NULL;
int nw_created = 0, nr_created = 0;
int rc = 0;
atomic_store(&g_stop, 0);
if (open_bench_pipe() < 0)
return -1;
wt = calloc((size_t)nw, sizeof(*wt));
rt = calloc((size_t)nr, sizeof(*rt));
ws = calloc((size_t)nw, sizeof(*ws));
rs = calloc((size_t)nr, sizeof(*rs));
if (!wt || !rt || !ws || !rs) {
fprintf(stderr, "alloc failed\n");
rc = -1;
goto teardown;
}
if (alloc_thread_bufs(ws, nw, rs, nr) < 0) {
rc = -1;
goto teardown;
}
if (start_readers(rt, rs, nr, &nr_created) < 0 ||
start_writers(wt, ws, nw, &nw_created) < 0) {
rc = -1;
goto teardown;
}
sleep((unsigned int)g_duration);
teardown:
stop_and_join(wt, nw_created, rt, nr_created, rc);
if (rc == 0) {
summarize(ws, nw, nr);
fflush(stdout);
}
free_thread_bufs(ws, nw, rs, nr);
free(wt);
free(rt);
free(ws);
free(rs);
return rc;
}
static void usage(const char *prog)
{
fprintf(stderr,
"usage: %s [-w writers] [-r readers] [-s msgsize] [-d secs] [-p pipe_size] [--memory-pressure]\n"
" default: sweep writers={1,2,5} x readers={1,5,10}\n"
" --memory-pressure: spawn stress-ng (--vm 4 --vm-bytes 80%% --vm-method all) for the run\n",
prog);
}
static int parse_args(int argc, char **argv,
int *writers_override, int *readers_override)
{
static const struct option long_opts[] = {
{"memory-pressure", no_argument, NULL, 'M'},
{0, 0, 0, 0},
};
int opt;
while ((opt = getopt_long(argc, argv, "w:r:s:d:p:",
long_opts, NULL)) != -1) {
switch (opt) {
case 'w':
*writers_override = atoi(optarg);
break;
case 'r':
*readers_override = atoi(optarg);
break;
case 's':
g_msgsize = (size_t)atol(optarg);
break;
case 'd':
g_duration = atoi(optarg);
break;
case 'p':
g_pipe_size = atoi(optarg);
break;
case 'M':
g_memory_pressure = 1;
break;
default:
usage(argv[0]);
return -1;
}
}
return 0;
}
static int validate_args(void)
{
if (g_msgsize == 0 || g_msgsize % 4096 != 0) {
fprintf(stderr,
"msgsize must be a positive multiple of 4096 (got %zu)\n",
g_msgsize);
return -1;
}
if (g_duration <= 0) {
fprintf(stderr, "duration must be > 0 seconds (got %d)\n",
g_duration);
return -1;
}
if (g_pipe_size <= 0) {
fprintf(stderr, "pipe_size must be > 0 bytes (got %d)\n",
g_pipe_size);
return -1;
}
return 0;
}
static int run_sweep(void)
{
static const int writers_sweep[] = {1, 2, 5};
static const int readers_sweep[] = {1, 5, 10};
for (size_t i = 0; i < ARRAY_SIZE(writers_sweep); i++) {
for (size_t j = 0; j < ARRAY_SIZE(readers_sweep); j++) {
printf("---\n");
if (run_one(writers_sweep[i], readers_sweep[j]) < 0)
return -1;
}
}
return 0;
}
int main(int argc, char **argv)
{
int writers_override = 0, readers_override = 0;
pid_t stress_pid = -1;
int rc = 0;
if (parse_args(argc, argv, &writers_override, &readers_override) < 0)
return 1;
if (validate_args() < 0)
return 1;
signal(SIGPIPE, SIG_IGN);
setvbuf(stdout, NULL, _IOLBF, 0);
setvbuf(stderr, NULL, _IOLBF, 0);
fprintf(stderr, "pid=%d\n", getpid());
fflush(stderr);
if (g_memory_pressure) {
stress_pid = spawn_stress_ng();
if (stress_pid < 0) {
fprintf(stderr,
"memory_pressure requested but stress-ng could not be spawned\n");
return 1;
}
}
if (writers_override > 0 || readers_override > 0) {
int nw = writers_override > 0 ? writers_override : 1;
int nr = readers_override > 0 ? readers_override : 1;
rc = run_one(nw, nr) < 0 ? 1 : 0;
} else {
rc = run_sweep() < 0 ? 1 : 0;
}
kill_stress_ng(stress_pid);
return rc;
}