#include "test/jemalloc_test.h"

#include "jemalloc/internal/edata_cache.h"

static void
test_edata_cache_init(edata_cache_t *edata_cache) {
	base_t *base = base_new(TSDN_NULL, /* ind */ 1,
	    &ehooks_default_extent_hooks, /* metadata_use_hooks */ true);
	assert_ptr_not_null(base, "");
	bool err = edata_cache_init(edata_cache, base);
	assert_false(err, "");
}

static void
test_edata_cache_destroy(edata_cache_t *edata_cache) {
	base_delete(TSDN_NULL, edata_cache->base);
}

TEST_BEGIN(test_edata_cache) {
	edata_cache_t ec;
	test_edata_cache_init(&ec);

	/* Get one */
	edata_t *ed1 = edata_cache_get(TSDN_NULL, &ec);
	assert_ptr_not_null(ed1, "");

	/* Cache should be empty */
	assert_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	/* Get another */
	edata_t *ed2 = edata_cache_get(TSDN_NULL, &ec);
	assert_ptr_not_null(ed2, "");

	/* Still empty */
	assert_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	/* Put one back, and the cache should now have one item */
	edata_cache_put(TSDN_NULL, &ec, ed1);
	assert_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 1, "");

	/* Reallocating should reuse the item, and leave an empty cache. */
	edata_t *ed1_again = edata_cache_get(TSDN_NULL, &ec);
	assert_ptr_eq(ed1, ed1_again, "");
	assert_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	test_edata_cache_destroy(&ec);
}
TEST_END

static size_t
ecf_count(edata_cache_fast_t *ecf) {
	size_t   count = 0;
	edata_t *cur;
	ql_foreach (cur, &ecf->list.head, ql_link_inactive) {
		count++;
	}
	return count;
}

TEST_BEGIN(test_edata_cache_fast_simple) {
	edata_cache_t      ec;
	edata_cache_fast_t ecf;

	test_edata_cache_init(&ec);
	edata_cache_fast_init(&ecf, &ec);

	edata_t *ed1 = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_ptr_not_null(ed1, "");
	expect_zu_eq(ecf_count(&ecf), 0, "");
	expect_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	edata_t *ed2 = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_ptr_not_null(ed2, "");
	expect_zu_eq(ecf_count(&ecf), 0, "");
	expect_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	edata_cache_fast_put(TSDN_NULL, &ecf, ed1);
	expect_zu_eq(ecf_count(&ecf), 1, "");
	expect_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	edata_cache_fast_put(TSDN_NULL, &ecf, ed2);
	expect_zu_eq(ecf_count(&ecf), 2, "");
	expect_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	/* LIFO ordering. */
	expect_ptr_eq(ed2, edata_cache_fast_get(TSDN_NULL, &ecf), "");
	expect_zu_eq(ecf_count(&ecf), 1, "");
	expect_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	expect_ptr_eq(ed1, edata_cache_fast_get(TSDN_NULL, &ecf), "");
	expect_zu_eq(ecf_count(&ecf), 0, "");
	expect_zu_eq(atomic_load_zu(&ec.count, ATOMIC_RELAXED), 0, "");

	test_edata_cache_destroy(&ec);
}
TEST_END

TEST_BEGIN(test_edata_cache_fill) {
	edata_cache_t      ec;
	edata_cache_fast_t ecf;

	test_edata_cache_init(&ec);
	edata_cache_fast_init(&ecf, &ec);

	edata_t *allocs[EDATA_CACHE_FAST_FILL * 2];

	/*
	 * If the fallback cache can't satisfy the request, we shouldn't do
	 * extra allocations until compelled to.  Put half the fill goal in the
	 * fallback.
	 */
	for (int i = 0; i < EDATA_CACHE_FAST_FILL / 2; i++) {
		allocs[i] = edata_cache_get(TSDN_NULL, &ec);
	}
	for (int i = 0; i < EDATA_CACHE_FAST_FILL / 2; i++) {
		edata_cache_put(TSDN_NULL, &ec, allocs[i]);
	}
	expect_zu_eq(EDATA_CACHE_FAST_FILL / 2,
	    atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");

	allocs[0] = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_zu_eq(EDATA_CACHE_FAST_FILL / 2 - 1, ecf_count(&ecf),
	    "Should have grabbed all edatas available but no more.");

	for (int i = 1; i < EDATA_CACHE_FAST_FILL / 2; i++) {
		allocs[i] = edata_cache_fast_get(TSDN_NULL, &ecf);
		expect_ptr_not_null(allocs[i], "");
	}
	expect_zu_eq(0, ecf_count(&ecf), "");

	/* When forced, we should alloc from the base. */
	edata_t *edata = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_ptr_not_null(edata, "");
	expect_zu_eq(0, ecf_count(&ecf), "Allocated more than necessary");
	expect_zu_eq(0, atomic_load_zu(&ec.count, ATOMIC_RELAXED),
	    "Allocated more than necessary");

	/*
	 * We should correctly fill in the common case where the fallback isn't
	 * exhausted, too.
	 */
	for (int i = 0; i < EDATA_CACHE_FAST_FILL * 2; i++) {
		allocs[i] = edata_cache_get(TSDN_NULL, &ec);
		expect_ptr_not_null(allocs[i], "");
	}
	for (int i = 0; i < EDATA_CACHE_FAST_FILL * 2; i++) {
		edata_cache_put(TSDN_NULL, &ec, allocs[i]);
	}

	allocs[0] = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_zu_eq(EDATA_CACHE_FAST_FILL - 1, ecf_count(&ecf), "");
	expect_zu_eq(EDATA_CACHE_FAST_FILL,
	    atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");
	for (int i = 1; i < EDATA_CACHE_FAST_FILL; i++) {
		expect_zu_eq(EDATA_CACHE_FAST_FILL - i, ecf_count(&ecf), "");
		expect_zu_eq(EDATA_CACHE_FAST_FILL,
		    atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");
		allocs[i] = edata_cache_fast_get(TSDN_NULL, &ecf);
		expect_ptr_not_null(allocs[i], "");
	}
	expect_zu_eq(0, ecf_count(&ecf), "");
	expect_zu_eq(EDATA_CACHE_FAST_FILL,
	    atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");

	allocs[0] = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_zu_eq(EDATA_CACHE_FAST_FILL - 1, ecf_count(&ecf), "");
	expect_zu_eq(0, atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");
	for (int i = 1; i < EDATA_CACHE_FAST_FILL; i++) {
		expect_zu_eq(EDATA_CACHE_FAST_FILL - i, ecf_count(&ecf), "");
		expect_zu_eq(0, atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");
		allocs[i] = edata_cache_fast_get(TSDN_NULL, &ecf);
		expect_ptr_not_null(allocs[i], "");
	}
	expect_zu_eq(0, ecf_count(&ecf), "");
	expect_zu_eq(0, atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");

	test_edata_cache_destroy(&ec);
}
TEST_END

TEST_BEGIN(test_edata_cache_disable) {
	edata_cache_t      ec;
	edata_cache_fast_t ecf;

	test_edata_cache_init(&ec);
	edata_cache_fast_init(&ecf, &ec);

	for (int i = 0; i < EDATA_CACHE_FAST_FILL; i++) {
		edata_t *edata = edata_cache_get(TSDN_NULL, &ec);
		expect_ptr_not_null(edata, "");
		edata_cache_fast_put(TSDN_NULL, &ecf, edata);
	}

	expect_zu_eq(EDATA_CACHE_FAST_FILL, ecf_count(&ecf), "");
	expect_zu_eq(0, atomic_load_zu(&ec.count, ATOMIC_RELAXED), "");

	edata_cache_fast_disable(TSDN_NULL, &ecf);

	expect_zu_eq(0, ecf_count(&ecf), "");
	expect_zu_eq(EDATA_CACHE_FAST_FILL,
	    atomic_load_zu(&ec.count, ATOMIC_RELAXED),
	    "Disabling should flush");

	edata_t *edata = edata_cache_fast_get(TSDN_NULL, &ecf);
	expect_zu_eq(0, ecf_count(&ecf), "");
	expect_zu_eq(EDATA_CACHE_FAST_FILL - 1,
	    atomic_load_zu(&ec.count, ATOMIC_RELAXED),
	    "Disabled ecf should forward on get");

	edata_cache_fast_put(TSDN_NULL, &ecf, edata);
	expect_zu_eq(0, ecf_count(&ecf), "");
	expect_zu_eq(EDATA_CACHE_FAST_FILL,
	    atomic_load_zu(&ec.count, ATOMIC_RELAXED),
	    "Disabled ecf should forward on put");

	test_edata_cache_destroy(&ec);
}
TEST_END

int
main(void) {
	return test(test_edata_cache, test_edata_cache_fast_simple,
	    test_edata_cache_fill, test_edata_cache_disable);
}
