/*
 * A simple test of forward/backward sequential scans with direct I/O.
 *
 * Build like this:
 *
 *		gcc -O2 -Werror -o direct-io-test direct-io-test.c
 *
 * Use like this:
 *
 * 		./direct-io-test /PATH/TO/FILE SIZE TIME_LIMIT
 *
 * with size in gigabytes (e.g. 32 means 32GB). The file will be created
 * and populated with random data. Then it runs a number of test with
 * different block sizes (1KB to 8MB) and directions. Can take a lot of
 * time (1h or more, depending on the storage device).
 */
#define _GNU_SOURCE

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>

#include <sys/time.h>

/*
 * generate a file with size_gb size, fill it with random data
 */
void
generate_file(char *path, int size_gb)
{
/* longs to generate / write at once */
#define	BUFF_SIZE	1024

	long   *buff;
	size_t	buff_bytes = BUFF_SIZE * sizeof(long);
	long	size = (size_gb * 1024L * 1024L * 1024L);
	long	nwritten = 0;

	int		fd = open(path, (O_WRONLY | O_CREAT | O_DIRECT), (S_IRUSR | S_IRUSR));

	long	path_memalign = pathconf(path,_PC_REC_XFER_ALIGN);

	if (posix_memalign((void **) &buff, path_memalign, buff_bytes) != 0)
	{
		printf("posix_memalign failed (bytes %u memalign %ld)\n", buff_bytes, path_memalign);
		fflush(NULL);
		abort();
	}

	while (nwritten < size)
	{
		for (int i = 0; i < BUFF_SIZE; i++)
		{
			/* combine two 48-bit values into a 64-bit one */
			buff[i] = ((lrand48() << 16) ^ lrand48());
		}

		write(fd, buff, buff_bytes);

		nwritten += buff_bytes;
	}

	free(buff);

	fsync(fd);
	close(fd);
}

/*
 * Run forward/backward sequential scans, starting from 8MB blocks, and
 * going down to 1KB blocks. For each block test forward/backward scan.
 */
void
test_direct_io(char *path, int size_gb, int run, int run_limit)
{
	int		bs;	/* block size (in KB) */
	int		fd = open(path, (O_RDONLY | O_DIRECT), 0);
	long	size_kbs = (size_gb * 1024L * 1024L);
	long	size = (size_kbs * 1024L);

	long path_memalign = pathconf(path,_PC_REC_XFER_ALIGN);

	/* print header */
	printf("%16s%8s%8s%16s%8s%16s\n", "direction", "run", "block", "usec", "sec", "KB/s");
	fflush(NULL);

	/* start from largest block, go down to 1KB */
	bs = 8192;
	while (bs > 0)
	{
		char   *buff = NULL;
		long	block_bytes = (bs * 1024L);
		long	nblocks = (size / block_bytes);	/* total */
		long	nblocks_read;

#define MIN_CHECK_DISTANCE 1000

		long	nblocks_check;
		off_t	pos;

		/* timing */
		struct timeval	ts,
						te;
		long	usecs;
		double	secs;

		posix_memalign((void **) &buff, path_memalign, block_bytes);

		/* forward direction */
		gettimeofday(&ts, NULL);

		/* random starting position in the file (multiple of block size) */
		pos = (lrand48() % nblocks) * block_bytes;

		nblocks_read = 0;
		nblocks_check = MIN_CHECK_DISTANCE;
		while (nblocks_read < nblocks)
		{
			size_t	r = pread(fd, buff, block_bytes, pos);

			if (r == -1)
			{
				printf("pread failed %ld %m\n", r);
				fflush(NULL);
				abort();
			}
			else if (r < block_bytes)	/* incomplete read, just retry */
				continue;

			pos += block_bytes;

			if (pos >= size)
				pos = 0;

			nblocks_read++;

			/* maybe check time limit */
			if (nblocks_check == nblocks_read)
			{
				long	nblocks_expected;

				gettimeofday(&te, NULL);

				usecs = (te.tv_sec - ts.tv_sec) * 1000000L + (te.tv_usec - ts.tv_usec);
				secs = (double) usecs / 1000000L;

				/* run exceeded limit */
				if (secs > run_limit)
					break;

				/* how many blocks we expect to read in the limit */
				nblocks_expected = run_limit * (nblocks_read / secs);
				nblocks_check = (nblocks_expected + nblocks_read) / 2;

				if (nblocks_check < nblocks_read + MIN_CHECK_DISTANCE)
					nblocks_check = nblocks_read + MIN_CHECK_DISTANCE;
			}
		}

		gettimeofday(&te, NULL);

		usecs = (te.tv_sec - ts.tv_sec) * 1000000L + (te.tv_usec - ts.tv_usec);
		secs = (double) usecs / 1000000L;

		/* print info about the forward scan */
		printf("%16s%8d%8d%16ld%8.2f%16.2f\n", "forward", run, bs, usecs, secs,
			   (nblocks_read * block_bytes / 1024L) / secs);
		fflush(NULL);

		/* backward direction */
		gettimeofday(&ts, NULL);

		/* random starting position in the file (multiple of block size) */
		pos = (lrand48() % nblocks) * block_bytes;

		nblocks_read = 0;
		nblocks_check = MIN_CHECK_DISTANCE;
		while (nblocks_read < nblocks)	/* number of blocks remaining */
		{
			size_t r = pread(fd, buff, block_bytes, pos);

			if (r == -1)
			{
				printf("pread failed %ld %m\n", r);
				fflush(NULL);
				abort();
			}
			else if (r < block_bytes)	/* incomplete read, just retry */
			{
				printf("retry write\n");
				fflush(NULL);
				continue;
			}

			pos -= block_bytes;

			if (pos < 0)
				pos = size - block_bytes;

			nblocks_read++;

			/* maybe check time limit */
			if (nblocks_check == nblocks_read)
			{
				long	nblocks_expected;

				gettimeofday(&te, NULL);

				usecs = (te.tv_sec - ts.tv_sec) * 1000000L + (te.tv_usec - ts.tv_usec);
				secs = (double) usecs / 1000000L;

				/* run exceeded limit */
				if (secs > run_limit)
					break;

				/* how many blocks we expect to read in the limit */
				nblocks_expected = run_limit * (nblocks_read / secs);
				nblocks_check = (nblocks_expected + nblocks_read) / 2;

				if (nblocks_check < nblocks_read + MIN_CHECK_DISTANCE)
					nblocks_check = nblocks_read + MIN_CHECK_DISTANCE;
			}
		}

		gettimeofday(&te, NULL);

		usecs = (te.tv_sec - ts.tv_sec) * 1000000L + (te.tv_usec - ts.tv_usec);
		secs = (double) usecs / 1000000L;

		/* print info about the backward scan */
		printf("%16s%8d%8d%16ld%8.2f%16.2f\n", "backward", run, bs, usecs, secs,
			   (nblocks_read * block_bytes / 1024L) / secs);
		fflush(NULL);

		free(buff);

		/* proceed to smaller block */
		bs /= 2;
	}

	close(fd);
}

int
main(int argc, char **argv)
{
	char *path = argv[1];
	int	size_gb = atoi(argv[2]);
	int	runs = atoi(argv[3]);
	int	limit = atoi(argv[4]);

	printf("path = %s\n", path);
	printf("size = %d GB\n", size_gb);
	printf("runs = %d\n", runs);
	printf("limit = %d\n", limit);

	fflush(NULL);

	generate_file(path, size_gb);

	printf("file generated\n");
	fflush(NULL);

	/* */
	for (int r = 1; r <= runs; r++)
		test_direct_io(path, size_gb, r, limit);

	return 0;
}
