/*
 * DF-3250 race driver - two processes, tight spin barrier, race open()
 * of the same (cold) ext2 inode via two DIFFERENT directories (hardlinks)
 * so the two lookups do not serialize on a common parent-dir lock.
 * Both threads miss ext2_ihashget and enter ext2_alloc_vnode; when the
 * loser's hash_lock check lands after the winner's unlock, ext2_ihashins
 * returns EBUSY and the handler at ext2_vfsops.c:1313-1320 double-frees
 * the inode -> KKASSERT bmap panic (KMALLOC_CHECK_DOUBLE_FREE).
 *
 * usage: race_e2 <dirA> <dirB> <nfiles> [max_seconds]
 *   thread0 opens <dirA>/rNNNNN, thread1 opens <dirB>/rNNNNN (same ino)
 */
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/usched.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

struct shared {
	volatile uint64_t narrate;	/* monotonic arrival count */
	volatile uint64_t ndone;	/* monotonic completion count */
	volatile uint32_t stop;
};

static inline void
cpu_pause(void)
{
	__asm__ volatile("pause");
}

static inline uint64_t
rdtsc(void)
{
	unsigned int lo, hi;

	__asm__ volatile("rdtsc" : "=a"(lo), "=d"(hi));
	return ((uint64_t)hi << 32) | lo;
}

int
main(int argc, char **argv)
{
	const char *dir;
	long nfiles;
	int id, fd;
	uint64_t k;
	time_t t0, tmax = 0;
	struct shared *s;
	char path[512];

	if (argc < 4) {
		fprintf(stderr, "usage: %s <dirA> <dirB> <nfiles> "
		    "[max_seconds]\n", argv[0]);
		return (2);
	}
	id = fork() != 0;			/* parent: 1, child: 0 */
	dir = id ? argv[2] : argv[1];		/* parent->dirB, child->dirA */
	nfiles = atol(argv[3]);
	if (argc > 4)
		tmax = atol(argv[4]);

	s = mmap(NULL, sizeof(*s), PROT_READ | PROT_WRITE,
	    MAP_SHARED | MAP_ANON, -1, 0);
	memset((void *)s, 0, sizeof(*s));

	id = fork() != 0;			/* parent: 1, child: 0 */

	{
		int cpu = id;
		if (usched_set(getpid(), USCHED_SET_CPU, &cpu,
		    sizeof(cpu)) != 0)
			fprintf(stderr, "id=%d: usched_set(cpu %d) FAILED "
			    "(errno-based pin fallback)\n", id, cpu);
		else
			fprintf(stderr, "id=%d pinned to cpu %d\n", id, cpu);
	}

	t0 = time(NULL);
	for (k = 0; ; k++) {
		uint64_t idx;

		if (s->stop)
			break;
		if (tmax && (uint64_t)(time(NULL) - t0) > (uint64_t)tmax) {
			s->stop = 1;
			break;
		}

		/* barrier: both arrive for round k */
		__sync_add_and_fetch(&s->narrate, 1);
		while (s->narrate < 2 * (k + 1))
			cpu_pause();

		/*
		 * Phase sweep: the parent (thread B, second arriver)
		 * delays (k mod 2000) * ~1400 tsc-cycles (approx 0..4ms
		 * at ~3GHz) before its open, so across rounds it samples
		 * every offset relative to the winner's whole cold-vget
		 * timeline (dir-cache misses included).
		 */
		if (id) {
			uint64_t spin = (k % 2000ULL) * 1400ULL;
			uint64_t tend = rdtsc() + spin;

			while (rdtsc() < tend)
				cpu_pause();
		}

		idx = k % (uint64_t)nfiles;
		snprintf(path, sizeof(path), "%s/r%05llu", dir,
		    (unsigned long long)idx);
		fd = open(path, O_RDONLY);
		if (fd >= 0)
			close(fd);

		/* completion barrier for round k */
		__sync_add_and_fetch(&s->ndone, 1);
		while (s->ndone < 2 * (k + 1))
			cpu_pause();

		if (id && (k % 20000) == 0)
			fprintf(stderr, "attempts=%llu elapsed=%ld\n",
			    (unsigned long long)k,
			    (long)(time(NULL) - t0));
	}
	if (id) {
		int st;
		wait(&st);
		fprintf(stderr, "total attempts ~%llu, exiting\n",
		    (unsigned long long)k);
	}
	return (0);
}
