#include "test/jemalloc_test.h"

#include "jemalloc/internal/prof_recent.h"

/* As specified in the shell script */
#define OPT_ALLOC_MAX 3

const char *test_thread_name = "test_thread";

/* Invariant before and after every test (when config_prof is on) */
static void
confirm_prof_setup(void) {
	/* Options */
	assert_true(opt_prof, "opt_prof not on");
	assert_true(opt_prof_active, "opt_prof_active not on");
	assert_zd_eq(opt_prof_recent_alloc_max, OPT_ALLOC_MAX,
	    "opt_prof_recent_alloc_max not set correctly");

	/* Dynamics */
	assert_true(prof_active_state, "prof_active not on");
	assert_zd_eq(prof_recent_alloc_max_ctl_read(), OPT_ALLOC_MAX,
	    "prof_recent_alloc_max not set correctly");
}

TEST_BEGIN(test_confirm_setup) {
	test_skip_if(!config_prof);
	confirm_prof_setup();
}
TEST_END

TEST_BEGIN(test_prof_recent_off) {
	test_skip_if(config_prof);

	const ssize_t past_ref = 0, future_ref = 0;
	const size_t  len_ref = sizeof(ssize_t);

	ssize_t past = past_ref, future = future_ref;
	size_t  len = len_ref;

#define ASSERT_SHOULD_FAIL(opt, a, b, c, d)                                    \
	do {                                                                   \
		assert_d_eq(                                                   \
		    mallctl("experimental.prof_recent." opt, a, b, c, d),      \
		    ENOENT, "Should return ENOENT when config_prof is off");   \
		assert_zd_eq(past, past_ref, "output was touched");            \
		assert_zu_eq(len, len_ref, "output length was touched");       \
		assert_zd_eq(future, future_ref, "input was touched");         \
	} while (0)

	ASSERT_SHOULD_FAIL("alloc_max", NULL, NULL, NULL, 0);
	ASSERT_SHOULD_FAIL("alloc_max", &past, &len, NULL, 0);
	ASSERT_SHOULD_FAIL("alloc_max", NULL, NULL, &future, len);
	ASSERT_SHOULD_FAIL("alloc_max", &past, &len, &future, len);

#undef ASSERT_SHOULD_FAIL
}
TEST_END

TEST_BEGIN(test_prof_recent_on) {
	test_skip_if(!config_prof);

	ssize_t past, future;
	size_t  len = sizeof(ssize_t);

	confirm_prof_setup();

	assert_d_eq(
	    mallctl("experimental.prof_recent.alloc_max", NULL, NULL, NULL, 0),
	    0, "no-op mallctl should be allowed");
	confirm_prof_setup();

	assert_d_eq(
	    mallctl("experimental.prof_recent.alloc_max", &past, &len, NULL, 0),
	    0, "Read error");
	expect_zd_eq(past, OPT_ALLOC_MAX, "Wrong read result");
	future = OPT_ALLOC_MAX + 1;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, len),
	    0, "Write error");
	future = -1;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", &past, &len,
	                &future, len),
	    0, "Read/write error");
	expect_zd_eq(past, OPT_ALLOC_MAX + 1, "Wrong read result");
	future = -2;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", &past, &len,
	                &future, len),
	    EINVAL, "Invalid write should return EINVAL");
	expect_zd_eq(past, OPT_ALLOC_MAX + 1,
	    "Output should not be touched given invalid write");
	future = OPT_ALLOC_MAX;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", &past, &len,
	                &future, len),
	    0, "Read/write error");
	expect_zd_eq(past, -1, "Wrong read result");
	future = OPT_ALLOC_MAX + 2;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", &past, &len,
	                &future, len * 2),
	    EINVAL, "Invalid write should return EINVAL");
	expect_zd_eq(
	    past, -1, "Output should not be touched given invalid write");

	confirm_prof_setup();
}
TEST_END

/* Reproducible sequence of request sizes */
#define NTH_REQ_SIZE(n) ((n) * 97 + 101)

static void
confirm_malloc(void *p) {
	assert_ptr_not_null(p, "malloc failed unexpectedly");
	edata_t *e = emap_edata_lookup(TSDN_NULL, &arena_emap_global, p);
	assert_ptr_not_null(e, "NULL edata for living pointer");
	prof_recent_t *n = edata_prof_recent_alloc_get_no_lock_test(e);
	assert_ptr_not_null(n, "Record in edata should not be NULL");
	expect_ptr_not_null(
	    n->alloc_tctx, "alloc_tctx in record should not be NULL");
	expect_ptr_eq(e, prof_recent_alloc_edata_get_no_lock_test(n),
	    "edata pointer in record is not correct");
	expect_ptr_null(n->dalloc_tctx, "dalloc_tctx in record should be NULL");
}

static void
confirm_record_size(prof_recent_t *n, unsigned kth) {
	expect_zu_eq(
	    n->size, NTH_REQ_SIZE(kth), "Recorded allocation size is wrong");
}

static void
confirm_record_living(prof_recent_t *n) {
	expect_ptr_not_null(
	    n->alloc_tctx, "alloc_tctx in record should not be NULL");
	edata_t *edata = prof_recent_alloc_edata_get_no_lock_test(n);
	assert_ptr_not_null(
	    edata, "Recorded edata should not be NULL for living pointer");
	expect_ptr_eq(n, edata_prof_recent_alloc_get_no_lock_test(edata),
	    "Record in edata is not correct");
	expect_ptr_null(n->dalloc_tctx, "dalloc_tctx in record should be NULL");
}

static void
confirm_record_released(prof_recent_t *n) {
	expect_ptr_not_null(
	    n->alloc_tctx, "alloc_tctx in record should not be NULL");
	expect_ptr_null(prof_recent_alloc_edata_get_no_lock_test(n),
	    "Recorded edata should be NULL for released pointer");
	expect_ptr_not_null(n->dalloc_tctx,
	    "dalloc_tctx in record should not be NULL for released pointer");
}

TEST_BEGIN(test_prof_recent_alloc) {
	test_skip_if(!config_prof);

	bool           b;
	unsigned       i, c;
	size_t         req_size;
	void          *p;
	prof_recent_t *n;
	ssize_t        future;

	confirm_prof_setup();

	/*
	 * First batch of 2 * OPT_ALLOC_MAX allocations.  After the
	 * (OPT_ALLOC_MAX - 1)'th allocation the recorded allocations should
	 * always be the last OPT_ALLOC_MAX allocations coming from here.
	 */
	for (i = 0; i < 2 * OPT_ALLOC_MAX; ++i) {
		req_size = NTH_REQ_SIZE(i);
		p = malloc(req_size);
		confirm_malloc(p);
		if (i < OPT_ALLOC_MAX - 1) {
			assert_false(ql_empty(&prof_recent_alloc_list),
			    "Empty recent allocation");
			free(p);
			/*
			 * The recorded allocations may still include some
			 * other allocations before the test run started,
			 * so keep allocating without checking anything.
			 */
			continue;
		}
		c = 0;
		ql_foreach (n, &prof_recent_alloc_list, link) {
			++c;
			confirm_record_size(n, i + c - OPT_ALLOC_MAX);
			if (c == OPT_ALLOC_MAX) {
				confirm_record_living(n);
			} else {
				confirm_record_released(n);
			}
		}
		assert_u_eq(
		    c, OPT_ALLOC_MAX, "Incorrect total number of allocations");
		free(p);
	}

	confirm_prof_setup();

	b = false;
	assert_d_eq(mallctl("prof.active", NULL, NULL, &b, sizeof(bool)), 0,
	    "mallctl for turning off prof_active failed");

	/*
	 * Second batch of OPT_ALLOC_MAX allocations.  Since prof_active is
	 * turned off, this batch shouldn't be recorded.
	 */
	for (; i < 3 * OPT_ALLOC_MAX; ++i) {
		req_size = NTH_REQ_SIZE(i);
		p = malloc(req_size);
		assert_ptr_not_null(p, "malloc failed unexpectedly");
		c = 0;
		ql_foreach (n, &prof_recent_alloc_list, link) {
			confirm_record_size(n, c + OPT_ALLOC_MAX);
			confirm_record_released(n);
			++c;
		}
		assert_u_eq(
		    c, OPT_ALLOC_MAX, "Incorrect total number of allocations");
		free(p);
	}

	b = true;
	assert_d_eq(mallctl("prof.active", NULL, NULL, &b, sizeof(bool)), 0,
	    "mallctl for turning on prof_active failed");

	confirm_prof_setup();

	/*
	 * Third batch of OPT_ALLOC_MAX allocations.  Since prof_active is
	 * turned back on, they should be recorded, and in the list of recorded
	 * allocations they should follow the first batch rather than the
	 * second batch.
	 */
	for (; i < 4 * OPT_ALLOC_MAX; ++i) {
		req_size = NTH_REQ_SIZE(i);
		p = malloc(req_size);
		confirm_malloc(p);
		c = 0;
		ql_foreach (n, &prof_recent_alloc_list, link) {
			++c;
			confirm_record_size(n,
			    /* Is the allocation from the third batch? */
			    i + c - OPT_ALLOC_MAX >= 3 * OPT_ALLOC_MAX
			        ?
			        /* If yes, then it's just recorded. */
			        i + c - OPT_ALLOC_MAX
			        :
			        /*
			     * Otherwise, it should come from the first batch
			     * instead of the second batch.
			     */
			        i + c - 2 * OPT_ALLOC_MAX);
			if (c == OPT_ALLOC_MAX) {
				confirm_record_living(n);
			} else {
				confirm_record_released(n);
			}
		}
		assert_u_eq(
		    c, OPT_ALLOC_MAX, "Incorrect total number of allocations");
		free(p);
	}

	/* Increasing the limit shouldn't alter the list of records. */
	future = OPT_ALLOC_MAX + 1;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	c = 0;
	ql_foreach (n, &prof_recent_alloc_list, link) {
		confirm_record_size(n, c + 3 * OPT_ALLOC_MAX);
		confirm_record_released(n);
		++c;
	}
	assert_u_eq(c, OPT_ALLOC_MAX, "Incorrect total number of allocations");

	/*
	 * Decreasing the limit shouldn't alter the list of records as long as
	 * the new limit is still no less than the length of the list.
	 */
	future = OPT_ALLOC_MAX;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	c = 0;
	ql_foreach (n, &prof_recent_alloc_list, link) {
		confirm_record_size(n, c + 3 * OPT_ALLOC_MAX);
		confirm_record_released(n);
		++c;
	}
	assert_u_eq(c, OPT_ALLOC_MAX, "Incorrect total number of allocations");

	/*
	 * Decreasing the limit should shorten the list of records if the new
	 * limit is less than the length of the list.
	 */
	future = OPT_ALLOC_MAX - 1;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	c = 0;
	ql_foreach (n, &prof_recent_alloc_list, link) {
		++c;
		confirm_record_size(n, c + 3 * OPT_ALLOC_MAX);
		confirm_record_released(n);
	}
	assert_u_eq(
	    c, OPT_ALLOC_MAX - 1, "Incorrect total number of allocations");

	/* Setting to unlimited shouldn't alter the list of records. */
	future = -1;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	c = 0;
	ql_foreach (n, &prof_recent_alloc_list, link) {
		++c;
		confirm_record_size(n, c + 3 * OPT_ALLOC_MAX);
		confirm_record_released(n);
	}
	assert_u_eq(
	    c, OPT_ALLOC_MAX - 1, "Incorrect total number of allocations");

	/* Downshift to only one record. */
	future = 1;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	assert_false(ql_empty(&prof_recent_alloc_list), "Recent list is empty");
	n = ql_first(&prof_recent_alloc_list);
	confirm_record_size(n, 4 * OPT_ALLOC_MAX - 1);
	confirm_record_released(n);
	n = ql_next(&prof_recent_alloc_list, n, link);
	assert_ptr_null(n, "Recent list should only contain one record");

	/* Completely turn off. */
	future = 0;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	assert_true(
	    ql_empty(&prof_recent_alloc_list), "Recent list should be empty");

	/* Restore the settings. */
	future = OPT_ALLOC_MAX;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	assert_true(
	    ql_empty(&prof_recent_alloc_list), "Recent list should be empty");

	confirm_prof_setup();
}
TEST_END

#undef NTH_REQ_SIZE

#define DUMP_OUT_SIZE 4096
static char   dump_out[DUMP_OUT_SIZE];
static size_t dump_out_len = 0;

static void
test_dump_write_cb(void *not_used, const char *str) {
	size_t len = strlen(str);
	assert(dump_out_len + len < DUMP_OUT_SIZE);
	memcpy(dump_out + dump_out_len, str, len + 1);
	dump_out_len += len;
}

static void
call_dump(void) {
	static void *in[2] = {test_dump_write_cb, NULL};
	dump_out_len = 0;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_dump", NULL, NULL,
	                in, sizeof(in)),
	    0, "Dump mallctl raised error");
}

typedef struct {
	size_t size;
	size_t usize;
	bool   released;
} confirm_record_t;

#define DUMP_ERROR "Dump output is wrong"

static void
confirm_record(const char *template, const confirm_record_t *records,
    const size_t n_records) {
	static const char *types[2] = {"alloc", "dalloc"};
	static char        buf[64];

	/*
	 * The template string would be in the form of:
	 * "{...,\"recent_alloc\":[]}",
	 * and dump_out would be in the form of:
	 * "{...,\"recent_alloc\":[...]}".
	 * Using "- 2" serves to cut right before the ending "]}".
	 */
	assert_d_eq(
	    memcmp(dump_out, template, strlen(template) - 2), 0, DUMP_ERROR);
	assert_d_eq(memcmp(dump_out + strlen(dump_out) - 2,
	                template + strlen(template) - 2, 2),
	    0, DUMP_ERROR);

	const char             *start = dump_out + strlen(template) - 2;
	const char             *end = dump_out + strlen(dump_out) - 2;
	const confirm_record_t *record;
	for (record = records; record < records + n_records; ++record) {
#define ASSERT_CHAR(c)                                                         \
	do {                                                                   \
		assert_true(start < end, DUMP_ERROR);                          \
		assert_c_eq(*start++, c, DUMP_ERROR);                          \
	} while (0)

#define ASSERT_STR(s)                                                          \
	do {                                                                   \
		const size_t len = strlen(s);                                  \
		assert_true(start + len <= end, DUMP_ERROR);                   \
		assert_d_eq(memcmp(start, s, len), 0, DUMP_ERROR);             \
		start += len;                                                  \
	} while (0)

#define ASSERT_FORMATTED_STR(s, ...)                                           \
	do {                                                                   \
		malloc_snprintf(buf, sizeof(buf), s, __VA_ARGS__);             \
		ASSERT_STR(buf);                                               \
	} while (0)

		if (record != records) {
			ASSERT_CHAR(',');
		}

		ASSERT_CHAR('{');

		ASSERT_STR("\"size\"");
		ASSERT_CHAR(':');
		ASSERT_FORMATTED_STR("%zu", record->size);
		ASSERT_CHAR(',');

		ASSERT_STR("\"usize\"");
		ASSERT_CHAR(':');
		ASSERT_FORMATTED_STR("%zu", record->usize);
		ASSERT_CHAR(',');

		ASSERT_STR("\"released\"");
		ASSERT_CHAR(':');
		ASSERT_STR(record->released ? "true" : "false");
		ASSERT_CHAR(',');

		const char **type = types;
		while (true) {
			ASSERT_FORMATTED_STR("\"%s_thread_uid\"", *type);
			ASSERT_CHAR(':');
			while (isdigit(*start)) {
				++start;
			}
			ASSERT_CHAR(',');

			if (thd_has_setname() && opt_prof_sys_thread_name) {
				ASSERT_FORMATTED_STR(
				    "\"%s_thread_name\"", *type);
				ASSERT_FORMATTED_STR(
				    ":\"%s\",", test_thread_name);
			}

			ASSERT_FORMATTED_STR("\"%s_time\"", *type);
			ASSERT_CHAR(':');
			while (isdigit(*start)) {
				++start;
			}
			ASSERT_CHAR(',');

			ASSERT_FORMATTED_STR("\"%s_trace\"", *type);
			ASSERT_CHAR(':');
			ASSERT_CHAR('[');
			while (isdigit(*start) || *start == 'x'
			    || (*start >= 'a' && *start <= 'f')
			    || *start == '\"' || *start == ',') {
				++start;
			}
			ASSERT_CHAR(']');

			if (strcmp(*type, "dalloc") == 0) {
				break;
			}

			assert(strcmp(*type, "alloc") == 0);
			if (!record->released) {
				break;
			}

			ASSERT_CHAR(',');
			++type;
		}

		ASSERT_CHAR('}');

#undef ASSERT_FORMATTED_STR
#undef ASSERT_STR
#undef ASSERT_CHAR
	}
	assert_ptr_eq(record, records + n_records, DUMP_ERROR);
	assert_ptr_eq(start, end, DUMP_ERROR);
}

TEST_BEGIN(test_prof_recent_alloc_dump) {
	test_skip_if(!config_prof);

	thd_setname(test_thread_name);
	confirm_prof_setup();

	ssize_t          future;
	void            *p, *q;
	confirm_record_t records[2];

	assert_zu_eq(
	    lg_prof_sample, (size_t)0, "lg_prof_sample not set correctly");

	future = 0;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	call_dump();
	expect_str_eq(dump_out,
	    "{\"sample_interval\":1,"
	    "\"recent_alloc_max\":0,\"recent_alloc\":[]}",
	    DUMP_ERROR);

	future = 2;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	call_dump();
	const char *template =
	    "{\"sample_interval\":1,"
	    "\"recent_alloc_max\":2,\"recent_alloc\":[]}";
	expect_str_eq(dump_out, template, DUMP_ERROR);

	p = malloc(7);
	call_dump();
	records[0].size = 7;
	records[0].usize = sz_s2u(7);
	records[0].released = false;
	confirm_record(template, records, 1);

	q = mallocx(17, MALLOCX_ALIGN(128));
	call_dump();
	records[1].size = 17;
	records[1].usize = sz_sa2u(17, 128);
	records[1].released = false;
	confirm_record(template, records, 2);

	free(q);
	call_dump();
	records[1].released = true;
	confirm_record(template, records, 2);

	free(p);
	call_dump();
	records[0].released = true;
	confirm_record(template, records, 2);

	future = OPT_ALLOC_MAX;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &future, sizeof(ssize_t)),
	    0, "Write error");
	confirm_prof_setup();
}
TEST_END

#undef DUMP_ERROR
#undef DUMP_OUT_SIZE

#define N_THREADS 8
#define N_PTRS 512
#define N_CTLS 8
#define N_ITERS 2048
#define STRESS_ALLOC_MAX 4096

typedef struct {
	thd_t  thd;
	size_t id;
	void  *ptrs[N_PTRS];
	size_t count;
} thd_data_t;

static thd_data_t thd_data[N_THREADS];
static ssize_t    test_max;

static void
test_write_cb(void *cbopaque, const char *str) {
	sleep_ns(1000 * 1000);
}

static void *
f_thread(void *arg) {
	const size_t thd_id = *(size_t *)arg;
	thd_data_t  *data_p = thd_data + thd_id;
	assert(data_p->id == thd_id);
	data_p->count = 0;
	uint64_t rand = (uint64_t)thd_id;
	tsd_t   *tsd = tsd_fetch();
	assert(test_max > 1);
	ssize_t last_max = -1;
	for (int i = 0; i < N_ITERS; i++) {
		rand = prng_range_u64(&rand, N_PTRS + N_CTLS * 5);
		assert(data_p->count <= N_PTRS);
		if (rand < data_p->count) {
			assert(data_p->count > 0);
			if (rand != data_p->count - 1) {
				assert(data_p->count > 1);
				void *temp = data_p->ptrs[rand];
				data_p->ptrs[rand] =
				    data_p->ptrs[data_p->count - 1];
				data_p->ptrs[data_p->count - 1] = temp;
			}
			free(data_p->ptrs[--data_p->count]);
		} else if (rand < N_PTRS) {
			assert(data_p->count < N_PTRS);
			data_p->ptrs[data_p->count++] = malloc(1);
		} else if (rand % 5 == 0) {
			prof_recent_alloc_dump(tsd, test_write_cb, NULL);
		} else if (rand % 5 == 1) {
			last_max = prof_recent_alloc_max_ctl_read();
		} else if (rand % 5 == 2) {
			last_max = prof_recent_alloc_max_ctl_write(
			    tsd, test_max * 2);
		} else if (rand % 5 == 3) {
			last_max = prof_recent_alloc_max_ctl_write(
			    tsd, test_max);
		} else {
			assert(rand % 5 == 4);
			last_max = prof_recent_alloc_max_ctl_write(
			    tsd, test_max / 2);
		}
		assert_zd_ge(last_max, -1, "Illegal last-N max");
	}

	while (data_p->count > 0) {
		free(data_p->ptrs[--data_p->count]);
	}

	return NULL;
}

TEST_BEGIN(test_prof_recent_stress) {
	test_skip_if(!config_prof);

	confirm_prof_setup();

	test_max = OPT_ALLOC_MAX;
	for (size_t i = 0; i < N_THREADS; i++) {
		thd_data_t *data_p = thd_data + i;
		data_p->id = i;
		thd_create(&data_p->thd, &f_thread, &data_p->id);
	}
	for (size_t i = 0; i < N_THREADS; i++) {
		thd_data_t *data_p = thd_data + i;
		thd_join(data_p->thd, NULL);
	}

	test_max = STRESS_ALLOC_MAX;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &test_max, sizeof(ssize_t)),
	    0, "Write error");
	for (size_t i = 0; i < N_THREADS; i++) {
		thd_data_t *data_p = thd_data + i;
		data_p->id = i;
		thd_create(&data_p->thd, &f_thread, &data_p->id);
	}
	for (size_t i = 0; i < N_THREADS; i++) {
		thd_data_t *data_p = thd_data + i;
		thd_join(data_p->thd, NULL);
	}

	test_max = OPT_ALLOC_MAX;
	assert_d_eq(mallctl("experimental.prof_recent.alloc_max", NULL, NULL,
	                &test_max, sizeof(ssize_t)),
	    0, "Write error");
	confirm_prof_setup();
}
TEST_END

#undef STRESS_ALLOC_MAX
#undef N_ITERS
#undef N_PTRS
#undef N_THREADS

int
main(void) {
	return test(test_confirm_setup, test_prof_recent_off,
	    test_prof_recent_on, test_prof_recent_alloc,
	    test_prof_recent_alloc_dump, test_prof_recent_stress);
}
