// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2020 Facebook */
#include <asm/barrier.h>
#include <linux/perf_event.h>
#include <linux/ring_buffer.h>
#include <sys/epoll.h>
#include <sys/mman.h>
#include <argp.h>
#include <stdlib.h>
#include "bench.h"
#include "ringbuf_bench.skel.h"
#include "perfbuf_bench.skel.h"

static struct {
	bool back2back;
	int batch_cnt;
	bool sampled;
	int sample_rate;
	int ringbuf_sz; /* per-ringbuf, in bytes */
	bool ringbuf_use_output; /* use slower output API */
	int perfbuf_sz; /* per-CPU size, in pages */
} args = {
	.back2back = false,
	.batch_cnt = 500,
	.sampled = false,
	.sample_rate = 500,
	.ringbuf_sz = 512 * 1024,
	.ringbuf_use_output = false,
	.perfbuf_sz = 128,
};

enum {
	ARG_RB_BACK2BACK = 2000,
	ARG_RB_USE_OUTPUT = 2001,
	ARG_RB_BATCH_CNT = 2002,
	ARG_RB_SAMPLED = 2003,
	ARG_RB_SAMPLE_RATE = 2004,
};

static const struct argp_option opts[] = {
	{ "rb-b2b", ARG_RB_BACK2BACK, NULL, 0, "Back-to-back mode"},
	{ "rb-use-output", ARG_RB_USE_OUTPUT, NULL, 0, "Use bpf_ringbuf_output() instead of bpf_ringbuf_reserve()"},
	{ "rb-batch-cnt", ARG_RB_BATCH_CNT, "CNT", 0, "Set BPF-side record batch count"},
	{ "rb-sampled", ARG_RB_SAMPLED, NULL, 0, "Notification sampling"},
	{ "rb-sample-rate", ARG_RB_SAMPLE_RATE, "RATE", 0, "Notification sample rate"},
	{},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
	switch (key) {
	case ARG_RB_BACK2BACK:
		args.back2back = true;
		break;
	case ARG_RB_USE_OUTPUT:
		args.ringbuf_use_output = true;
		break;
	case ARG_RB_BATCH_CNT:
		args.batch_cnt = strtol(arg, NULL, 10);
		if (args.batch_cnt < 0) {
			fprintf(stderr, "Invalid batch count.");
			argp_usage(state);
		}
		break;
	case ARG_RB_SAMPLED:
		args.sampled = true;
		break;
	case ARG_RB_SAMPLE_RATE:
		args.sample_rate = strtol(arg, NULL, 10);
		if (args.sample_rate < 0) {
			fprintf(stderr, "Invalid perfbuf sample rate.");
			argp_usage(state);
		}
		break;
	default:
		return ARGP_ERR_UNKNOWN;
	}
	return 0;
}

/* exported into benchmark runner */
const struct argp bench_ringbufs_argp = {
	.options = opts,
	.parser = parse_arg,
};

/* RINGBUF-LIBBPF benchmark */

static struct counter buf_hits;

static inline void bufs_trigger_batch(void)
{
	(void)syscall(__NR_getpgid);
}

static void bufs_validate(void)
{
	if (env.consumer_cnt != 1) {
		fprintf(stderr, "rb-libbpf benchmark doesn't support multi-consumer!\n");
		exit(1);
	}

	if (args.back2back && env.producer_cnt > 1) {
		fprintf(stderr, "back-to-back mode makes sense only for single-producer case!\n");
		exit(1);
	}
}

static void *bufs_sample_producer(void *input)
{
	if (args.back2back) {
		/* initial batch to get everything started */
		bufs_trigger_batch();
		return NULL;
	}

	while (true)
		bufs_trigger_batch();
	return NULL;
}

static struct ringbuf_libbpf_ctx {
	struct ringbuf_bench *skel;
	struct ring_buffer *ringbuf;
} ringbuf_libbpf_ctx;

static void ringbuf_libbpf_measure(struct bench_res *res)
{
	struct ringbuf_libbpf_ctx *ctx = &ringbuf_libbpf_ctx;

	res->hits = atomic_swap(&buf_hits.value, 0);
	res->drops = atomic_swap(&ctx->skel->bss->dropped, 0);
}

static struct ringbuf_bench *ringbuf_setup_skeleton(void)
{
	struct ringbuf_bench *skel;

	setup_libbpf();

	skel = ringbuf_bench__open();
	if (!skel) {
		fprintf(stderr, "failed to open skeleton\n");
		exit(1);
	}

	skel->rodata->batch_cnt = args.batch_cnt;
	skel->rodata->use_output = args.ringbuf_use_output ? 1 : 0;

	if (args.sampled)
		/* record data + header take 16 bytes */
		skel->rodata->wakeup_data_size = args.sample_rate * 16;

	bpf_map__set_max_entries(skel->maps.ringbuf, args.ringbuf_sz);

	if (ringbuf_bench__load(skel)) {
		fprintf(stderr, "failed to load skeleton\n");
		exit(1);
	}

	return skel;
}

static int buf_process_sample(void *ctx, void *data, size_t len)
{
	atomic_inc(&buf_hits.value);
	return 0;
}

static void ringbuf_libbpf_setup(void)
{
	struct ringbuf_libbpf_ctx *ctx = &ringbuf_libbpf_ctx;
	struct bpf_link *link;

	ctx->skel = ringbuf_setup_skeleton();
	ctx->ringbuf = ring_buffer__new(bpf_map__fd(ctx->skel->maps.ringbuf),
					buf_process_sample, NULL, NULL);
	if (!ctx->ringbuf) {
		fprintf(stderr, "failed to create ringbuf\n");
		exit(1);
	}

	link = bpf_program__attach(ctx->skel->progs.bench_ringbuf);
	if (!link) {
		fprintf(stderr, "failed to attach program!\n");
		exit(1);
	}
}

static void *ringbuf_libbpf_consumer(void *input)
{
	struct ringbuf_libbpf_ctx *ctx = &ringbuf_libbpf_ctx;

	while (ring_buffer__poll(ctx->ringbuf, -1) >= 0) {
		if (args.back2back)
			bufs_trigger_batch();
	}
	fprintf(stderr, "ringbuf polling failed!\n");
	return NULL;
}

/* RINGBUF-CUSTOM benchmark */
struct ringbuf_custom {
	__u64 *consumer_pos;
	__u64 *producer_pos;
	__u64 mask;
	void *data;
	int map_fd;
};

static struct ringbuf_custom_ctx {
	struct ringbuf_bench *skel;
	struct ringbuf_custom ringbuf;
	int epoll_fd;
	struct epoll_event event;
} ringbuf_custom_ctx;

static void ringbuf_custom_measure(struct bench_res *res)
{
	struct ringbuf_custom_ctx *ctx = &ringbuf_custom_ctx;

	res->hits = atomic_swap(&buf_hits.value, 0);
	res->drops = atomic_swap(&ctx->skel->bss->dropped, 0);
}

static void ringbuf_custom_setup(void)
{
	struct ringbuf_custom_ctx *ctx = &ringbuf_custom_ctx;
	const size_t page_size = getpagesize();
	struct bpf_link *link;
	struct ringbuf_custom *r;
	void *tmp;
	int err;

	ctx->skel = ringbuf_setup_skeleton();

	ctx->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
	if (ctx->epoll_fd < 0) {
		fprintf(stderr, "failed to create epoll fd: %d\n", -errno);
		exit(1);
	}

	r = &ctx->ringbuf;
	r->map_fd = bpf_map__fd(ctx->skel->maps.ringbuf);
	r->mask = args.ringbuf_sz - 1;

	/* Map writable consumer page */
	tmp = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED,
		   r->map_fd, 0);
	if (tmp == MAP_FAILED) {
		fprintf(stderr, "failed to mmap consumer page: %d\n", -errno);
		exit(1);
	}
	r->consumer_pos = tmp;

	/* Map read-only producer page and data pages. */
	tmp = mmap(NULL, page_size + 2 * args.ringbuf_sz, PROT_READ, MAP_SHARED,
		   r->map_fd, page_size);
	if (tmp == MAP_FAILED) {
		fprintf(stderr, "failed to mmap data pages: %d\n", -errno);
		exit(1);
	}
	r->producer_pos = tmp;
	r->data = tmp + page_size;

	ctx->event.events = EPOLLIN;
	err = epoll_ctl(ctx->epoll_fd, EPOLL_CTL_ADD, r->map_fd, &ctx->event);
	if (err < 0) {
		fprintf(stderr, "failed to epoll add ringbuf: %d\n", -errno);
		exit(1);
	}

	link = bpf_program__attach(ctx->skel->progs.bench_ringbuf);
	if (!link) {
		fprintf(stderr, "failed to attach program\n");
		exit(1);
	}
}

#define RINGBUF_BUSY_BIT (1 << 31)
#define RINGBUF_DISCARD_BIT (1 << 30)
#define RINGBUF_META_LEN 8

static inline int roundup_len(__u32 len)
{
	/* clear out top 2 bits */
	len <<= 2;
	len >>= 2;
	/* add length prefix */
	len += RINGBUF_META_LEN;
	/* round up to 8 byte alignment */
	return (len + 7) / 8 * 8;
}

static void ringbuf_custom_process_ring(struct ringbuf_custom *r)
{
	unsigned long cons_pos, prod_pos;
	int *len_ptr, len;
	bool got_new_data;

	cons_pos = smp_load_acquire(r->consumer_pos);
	while (true) {
		got_new_data = false;
		prod_pos = smp_load_acquire(r->producer_pos);
		while (cons_pos < prod_pos) {
			len_ptr = r->data + (cons_pos & r->mask);
			len = smp_load_acquire(len_ptr);

			/* sample not committed yet, bail out for now */
			if (len & RINGBUF_BUSY_BIT)
				return;

			got_new_data = true;
			cons_pos += roundup_len(len);

			atomic_inc(&buf_hits.value);
		}
		if (got_new_data)
			smp_store_release(r->consumer_pos, cons_pos);
		else
			break;
	}
}

static void *ringbuf_custom_consumer(void *input)
{
	struct ringbuf_custom_ctx *ctx = &ringbuf_custom_ctx;
	int cnt;

	do {
		if (args.back2back)
			bufs_trigger_batch();
		cnt = epoll_wait(ctx->epoll_fd, &ctx->event, 1, -1);
		if (cnt > 0)
			ringbuf_custom_process_ring(&ctx->ringbuf);
	} while (cnt >= 0);
	fprintf(stderr, "ringbuf polling failed!\n");
	return 0;
}

/* PERFBUF-LIBBPF benchmark */
static struct perfbuf_libbpf_ctx {
	struct perfbuf_bench *skel;
	struct perf_buffer *perfbuf;
} perfbuf_libbpf_ctx;

static void perfbuf_measure(struct bench_res *res)
{
	struct perfbuf_libbpf_ctx *ctx = &perfbuf_libbpf_ctx;

	res->hits = atomic_swap(&buf_hits.value, 0);
	res->drops = atomic_swap(&ctx->skel->bss->dropped, 0);
}

static struct perfbuf_bench *perfbuf_setup_skeleton(void)
{
	struct perfbuf_bench *skel;

	setup_libbpf();

	skel = perfbuf_bench__open();
	if (!skel) {
		fprintf(stderr, "failed to open skeleton\n");
		exit(1);
	}

	skel->rodata->batch_cnt = args.batch_cnt;

	if (perfbuf_bench__load(skel)) {
		fprintf(stderr, "failed to load skeleton\n");
		exit(1);
	}

	return skel;
}

static enum bpf_perf_event_ret
perfbuf_process_sample_raw(void *input_ctx, int cpu,
			   struct perf_event_header *e)
{
	switch (e->type) {
	case PERF_RECORD_SAMPLE:
		atomic_inc(&buf_hits.value);
		break;
	case PERF_RECORD_LOST:
		break;
	default:
		return LIBBPF_PERF_EVENT_ERROR;
	}
	return LIBBPF_PERF_EVENT_CONT;
}

static void perfbuf_libbpf_setup(void)
{
	struct perfbuf_libbpf_ctx *ctx = &perfbuf_libbpf_ctx;
	struct perf_event_attr attr;
	struct bpf_link *link;

	ctx->skel = perfbuf_setup_skeleton();

	memset(&attr, 0, sizeof(attr));
	attr.config = PERF_COUNT_SW_BPF_OUTPUT,
	attr.type = PERF_TYPE_SOFTWARE;
	attr.sample_type = PERF_SAMPLE_RAW;
	/* notify only every Nth sample */
	if (args.sampled) {
		attr.sample_period = args.sample_rate;
		attr.wakeup_events = args.sample_rate;
	} else {
		attr.sample_period = 1;
		attr.wakeup_events = 1;
	}

	if (args.sample_rate > args.batch_cnt) {
		fprintf(stderr, "sample rate %d is too high for given batch count %d\n",
			args.sample_rate, args.batch_cnt);
		exit(1);
	}

	ctx->perfbuf = perf_buffer__new_raw(bpf_map__fd(ctx->skel->maps.perfbuf),
					    args.perfbuf_sz, &attr,
					    perfbuf_process_sample_raw, NULL, NULL);
	if (!ctx->perfbuf) {
		fprintf(stderr, "failed to create perfbuf\n");
		exit(1);
	}

	link = bpf_program__attach(ctx->skel->progs.bench_perfbuf);
	if (!link) {
		fprintf(stderr, "failed to attach program\n");
		exit(1);
	}
}

static void *perfbuf_libbpf_consumer(void *input)
{
	struct perfbuf_libbpf_ctx *ctx = &perfbuf_libbpf_ctx;

	while (perf_buffer__poll(ctx->perfbuf, -1) >= 0) {
		if (args.back2back)
			bufs_trigger_batch();
	}
	fprintf(stderr, "perfbuf polling failed!\n");
	return NULL;
}

/* PERFBUF-CUSTOM benchmark */

/* copies of internal libbpf definitions */
struct perf_cpu_buf {
	struct perf_buffer *pb;
	void *base; /* mmap()'ed memory */
	void *buf; /* for reconstructing segmented data */
	size_t buf_size;
	int fd;
	int cpu;
	int map_key;
};

struct perf_buffer {
	perf_buffer_event_fn event_cb;
	perf_buffer_sample_fn sample_cb;
	perf_buffer_lost_fn lost_cb;
	void *ctx; /* passed into callbacks */

	size_t page_size;
	size_t mmap_size;
	struct perf_cpu_buf **cpu_bufs;
	struct epoll_event *events;
	int cpu_cnt; /* number of allocated CPU buffers */
	int epoll_fd; /* perf event FD */
	int map_fd; /* BPF_MAP_TYPE_PERF_EVENT_ARRAY BPF map FD */
};

static void *perfbuf_custom_consumer(void *input)
{
	struct perfbuf_libbpf_ctx *ctx = &perfbuf_libbpf_ctx;
	struct perf_buffer *pb = ctx->perfbuf;
	struct perf_cpu_buf *cpu_buf;
	struct perf_event_mmap_page *header;
	size_t mmap_mask = pb->mmap_size - 1;
	struct perf_event_header *ehdr;
	__u64 data_head, data_tail;
	size_t ehdr_size;
	void *base;
	int i, cnt;

	while (true) {
		if (args.back2back)
			bufs_trigger_batch();
		cnt = epoll_wait(pb->epoll_fd, pb->events, pb->cpu_cnt, -1);
		if (cnt <= 0) {
			fprintf(stderr, "perf epoll failed: %d\n", -errno);
			exit(1);
		}

		for (i = 0; i < cnt; ++i) {
			cpu_buf = pb->events[i].data.ptr;
			header = cpu_buf->base;
			base = ((void *)header) + pb->page_size;

			data_head = ring_buffer_read_head(header);
			data_tail = header->data_tail;
			while (data_head != data_tail) {
				ehdr = base + (data_tail & mmap_mask);
				ehdr_size = ehdr->size;

				if (ehdr->type == PERF_RECORD_SAMPLE)
					atomic_inc(&buf_hits.value);

				data_tail += ehdr_size;
			}
			ring_buffer_write_tail(header, data_tail);
		}
	}
	return NULL;
}

const struct bench bench_rb_libbpf = {
	.name = "rb-libbpf",
	.validate = bufs_validate,
	.setup = ringbuf_libbpf_setup,
	.producer_thread = bufs_sample_producer,
	.consumer_thread = ringbuf_libbpf_consumer,
	.measure = ringbuf_libbpf_measure,
	.report_progress = hits_drops_report_progress,
	.report_final = hits_drops_report_final,
};

const struct bench bench_rb_custom = {
	.name = "rb-custom",
	.validate = bufs_validate,
	.setup = ringbuf_custom_setup,
	.producer_thread = bufs_sample_producer,
	.consumer_thread = ringbuf_custom_consumer,
	.measure = ringbuf_custom_measure,
	.report_progress = hits_drops_report_progress,
	.report_final = hits_drops_report_final,
};

const struct bench bench_pb_libbpf = {
	.name = "pb-libbpf",
	.validate = bufs_validate,
	.setup = perfbuf_libbpf_setup,
	.producer_thread = bufs_sample_producer,
	.consumer_thread = perfbuf_libbpf_consumer,
	.measure = perfbuf_measure,
	.report_progress = hits_drops_report_progress,
	.report_final = hits_drops_report_final,
};

const struct bench bench_pb_custom = {
	.name = "pb-custom",
	.validate = bufs_validate,
	.setup = perfbuf_libbpf_setup,
	.producer_thread = bufs_sample_producer,
	.consumer_thread = perfbuf_custom_consumer,
	.measure = perfbuf_measure,
	.report_progress = hits_drops_report_progress,
	.report_final = hits_drops_report_final,
};

