/* Benchmark ops/s for stat(2), getxattr(2) of a missing attribute
 * ("round-trips"), and cached 1MB pread(2) on a single file.
 * The file is created if absent and left in place after the run; an
 * existing file is used read-only but must be at least 1MB (else abort).
 * Usage: ./bench <file>
 * Compile: gcc -O2 -Wall -o bench bench.c -lpthread
 * Runs single-threaded, then scales threads 1,2,4,... up to nproc.
 *
 * Example output (8-cpu AWS box, file on ObjectiveFS/FUSE):
 *
 *   file: testfile  xattr: user.nonexistent  2.0s/test  8 cpus
 *
 *   all values are totals across threads
 *
 *    threads    stat ops/s  round-trips/s  total read GB/s
 *          1       1395960         179345            22.28
 *          2       2496894         398616            37.76
 *          4       3599197         651192            50.88
 *          8       4640089         581727            72.75
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <stdatomic.h>
#include <fcntl.h>
#include <errno.h>

static const char *path;
static const char *xname = "user.nonexistent";
static double duration = 2.0;

static atomic_int go, stop_flag;

static inline double now(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec * 1e-9;
}

#define READ_SIZE (1u << 20)

/* modes */
enum { M_STAT, M_XATTR_MISS, M_READ, M_COUNT };

struct warg {
    int mode;
    uint64_t ops;
};

static void *worker(void *p)
{
    struct warg *w = p;
    char buf[256];
    struct stat st;
    uint64_t n = 0;
    char *rbuf = NULL;
    int fd = -1;

    if (w->mode == M_READ) {
        rbuf = malloc(READ_SIZE);
        fd = open(path, O_RDONLY);
        if (!rbuf || fd < 0) { perror("open/malloc"); exit(1); }
    }

    while (!atomic_load_explicit(&go, memory_order_acquire))
        ;
    switch (w->mode) {
    case M_STAT:
        while (!atomic_load_explicit(&stop_flag, memory_order_relaxed)) {
            if (stat(path, &st) != 0) { perror("stat"); exit(1); }
            n++;
        }
        break;
    case M_XATTR_MISS:
        while (!atomic_load_explicit(&stop_flag, memory_order_relaxed)) {
            if (getxattr(path, xname, buf, sizeof buf) >= 0
                || errno != ENODATA) {
                perror("getxattr-miss (expected ENODATA)"); exit(1);
            }
            n++;
        }
        break;
    case M_READ:
        while (!atomic_load_explicit(&stop_flag, memory_order_relaxed)) {
            if (pread(fd, rbuf, READ_SIZE, 0) != READ_SIZE) {
                perror("pread"); exit(1);
            }
            n++;
        }
        break;
    }
    if (fd >= 0) close(fd);
    free(rbuf);
    w->ops = n;
    return NULL;
}

static double run(int mode, int nthreads)
{
    pthread_t tid[256];
    struct warg wa[256];

    atomic_store(&go, 0);
    atomic_store(&stop_flag, 0);
    for (int i = 0; i < nthreads; i++) {
        wa[i].mode = mode;
        wa[i].ops = 0;
        pthread_create(&tid[i], NULL, worker, &wa[i]);
    }
    double t0 = now();
    atomic_store_explicit(&go, 1, memory_order_release);
    struct timespec ts = { (time_t)duration,
                           (long)((duration - (time_t)duration) * 1e9) };
    nanosleep(&ts, NULL);
    atomic_store(&stop_flag, 1);
    double t1 = now();

    uint64_t total = 0;
    for (int i = 0; i < nthreads; i++) {
        pthread_join(tid[i], NULL);
        total += wa[i].ops;
    }
    return total / (t1 - t0);
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        return 1;
    }
    path = argv[1];

    int ncpu = (int)sysconf(_SC_NPROCESSORS_ONLN);
    printf("file: %s  xattr: %s  %.1fs/test  %d cpus\n\n",
           path, xname, duration, ncpu);

    /* create a 1MB file, or use an existing one if it is >= 1MB */
    struct stat st; char buf[256];
    int fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0644);
    if (fd >= 0) {
        if (ftruncate(fd, READ_SIZE) != 0) { perror("ftruncate"); return 1; }
    } else {
        if (errno != EEXIST) { perror("open"); return 1; }
        fd = open(path, O_RDONLY);
        if (fd < 0) { perror("open"); return 1; }
        if (fstat(fd, &st) != 0) { perror("fstat"); return 1; }
        if (st.st_size < READ_SIZE) {
            fprintf(stderr, "%s exists but is smaller than %u bytes; "
                    "remove it or pick another file\n", path, READ_SIZE);
            return 1;
        }
    }
    close(fd);
    /* the benchmark xattr must be missing */
    if (getxattr(path, xname, buf, sizeof buf) >= 0 || errno != ENODATA) {
        fprintf(stderr, "xattr %s exists on %s (or getxattr failed: %s); "
                "pick a missing one\n", xname, path, strerror(errno));
        return 1;
    }
    { /* warm page cache for the read test */
        char *rbuf = malloc(READ_SIZE);
        int fd = open(path, O_RDONLY);
        if (fd < 0 || pread(fd, rbuf, READ_SIZE, 0) != READ_SIZE) {
            perror("warmup read"); return 1;
        }
        close(fd); free(rbuf);
    }

    printf("all values are totals across threads\n\n");
    printf("%8s  %12s  %13s  %15s\n",
           "threads", "stat ops/s", "round-trips/s", "total read GB/s");
    for (int t = 1; t <= ncpu; t *= 2) {
        printf("%8d", t);
        printf("  %12.0f", run(M_STAT, t));
        fflush(stdout);
        printf("  %13.0f", run(M_XATTR_MISS, t));
        fflush(stdout);
        printf("  %15.2f\n", run(M_READ, t) * READ_SIZE / 1e9);
        if (t < ncpu && t * 2 > ncpu) t = ncpu / 2;
    }
    return 0;
}
