#include "test/jemalloc_test.h"

#include "jemalloc/internal/pa.h"

/*
 * pac_decay_deferred is JET_EXTERN (test-only; see src/pac.c) and has no public
 * header declaration, so declare it locally for the deterministic test below.
 */
extern void pac_decay_deferred(tsdn_t *tsdn, pac_t *pac,
    pac_purge_eagerness_t eagerness, pac_deferred_work_result_t *result);

#define PAC_TEST_ARENA_IND 124

static bool pac_test_dalloc_fail;
static bool pac_test_purge_lazy_fail;
static unsigned pac_test_alloc_calls;
static unsigned pac_test_dalloc_calls;
static unsigned pac_test_destroy_calls;
static unsigned pac_test_purge_lazy_calls;

static void
pac_test_hooks_reset(void) {
	pac_test_dalloc_fail = false;
	pac_test_purge_lazy_fail = false;
	pac_test_alloc_calls = 0;
	pac_test_dalloc_calls = 0;
	pac_test_destroy_calls = 0;
	pac_test_purge_lazy_calls = 0;
}

static void *
pac_test_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size,
    size_t alignment, bool *zero, bool *commit, unsigned arena_ind) {
	pac_test_alloc_calls++;
	void *ret = pages_map(new_addr, size, alignment, commit);
	return ret;
}

static bool
pac_test_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size,
    bool committed, unsigned arena_ind) {
	pac_test_dalloc_calls++;
	if (pac_test_dalloc_fail) {
		return true;
	}
	pages_unmap(addr, size);
	return false;
}

static void
pac_test_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size,
    bool committed, unsigned arena_ind) {
	pac_test_destroy_calls++;
	pages_unmap(addr, size);
}

static bool
pac_test_purge_lazy(extent_hooks_t *extent_hooks, void *addr, size_t size,
    size_t offset, size_t length, unsigned arena_ind) {
	pac_test_purge_lazy_calls++;
	return pac_test_purge_lazy_fail;
}

static bool
pac_test_split(extent_hooks_t *extent_hooks, void *addr, size_t size,
    size_t size_a, size_t size_b, bool committed, unsigned arena_ind) {
	return !maps_coalesce && !opt_retain;
}

static bool
pac_test_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a,
    void *addr_b, size_t size_b, bool committed, unsigned arena_ind) {
	return !maps_coalesce && !opt_retain;
}

static extent_hooks_t pac_test_hooks = {
	pac_test_alloc,
	pac_test_dalloc,
	pac_test_destroy,
	NULL, /* commit */
	NULL, /* decommit */
	pac_test_purge_lazy,
	NULL, /* purge_forced */
	pac_test_split,
	pac_test_merge
};

typedef struct pac_test_data_s pac_test_data_t;
struct pac_test_data_s {
	pa_shard_t shard;
	pa_central_t central;
	base_t *base;
	emap_t emap;
	pa_shard_stats_t stats;
	malloc_mutex_t stats_mtx;
	extent_hooks_t hooks;
};

static pac_test_data_t *
pac_test_data_init(bool custom_hooks, ssize_t dirty_decay_ms,
    ssize_t muzzy_decay_ms) {
	tsdn_t *tsdn = tsdn_fetch();
	pac_test_data_t *data = calloc(1, sizeof(*data));
	assert_ptr_not_null(data, "Unexpected calloc failure");

	if (custom_hooks) {
		memcpy(&data->hooks, &pac_test_hooks, sizeof(extent_hooks_t));
	} else {
		memcpy(&data->hooks, &ehooks_default_extent_hooks,
		    sizeof(extent_hooks_t));
	}

	data->base = base_new(tsdn, PAC_TEST_ARENA_IND, &data->hooks,
	    /* metadata_use_hooks */ true);
	assert_ptr_not_null(data->base, "Unexpected base_new failure");
	assert_false(emap_init(&data->emap, data->base, /* zeroed */ true),
	    "Unexpected emap_init failure");
	assert_false(malloc_mutex_init(&data->stats_mtx, "pac_test_stats",
	    WITNESS_RANK_ARENA_STATS, malloc_mutex_rank_exclusive),
	    "Unexpected stats mutex initialization failure");

	nstime_t time;
	nstime_init(&time, 0);
	assert_false(pa_central_init(&data->central, data->base,
	    /* hpa */ false, &hpa_hooks_default),
	    "Unexpected pa_central_init failure");
	assert_false(pa_shard_init(tsdn, &data->shard, &data->central,
	    &data->emap, data->base, PAC_TEST_ARENA_IND, &data->stats,
	    &data->stats_mtx, &time, SC_LARGE_MAXCLASS + PAGE,
	    dirty_decay_ms, muzzy_decay_ms),
	    "Unexpected pa_shard_init failure");

	return data;
}

static void
pac_decay_all_locked(pac_t *pac, extent_state_t state, bool fully_decay) {
	decay_t *decay;
	pac_decay_stats_t *decay_stats;
	ecache_t *ecache;
	if (state == extent_state_dirty) {
		decay = &pac->decay_dirty;
		decay_stats = &pac->stats->decay_dirty;
		ecache = &pac->ecache_dirty;
	} else {
		assert_d_eq(extent_state_muzzy, state,
		    "Only dirty and muzzy decay are supported");
		decay = &pac->decay_muzzy;
		decay_stats = &pac->stats->decay_muzzy;
		ecache = &pac->ecache_muzzy;
	}

	tsdn_t *tsdn = tsdn_fetch();
	malloc_mutex_lock(tsdn, &decay->mtx);
	pac_decay_all(tsdn, pac, decay, decay_stats, ecache, fully_decay);
	malloc_mutex_unlock(tsdn, &decay->mtx);
}

static void
pac_test_data_destroy(pac_test_data_t *data) {
	/*
	 * Decay below calls back into the test hooks; reset all hook state
	 * (including the fail flags) so teardown is unaffected by anything the
	 * preceding test toggled.
	 */
	pac_test_hooks_reset();
	pac_decay_all_locked(&data->shard.pac, extent_state_dirty,
	    /* fully_decay */ true);
	pac_decay_all_locked(&data->shard.pac, extent_state_muzzy,
	    /* fully_decay */ true);
	pa_shard_destroy(tsdn_fetch(), &data->shard);
	base_delete(tsdn_fetch(), data->base);
	/*
	 * pac operations populated the tsd rtree_ctx with leaf-node pointers
	 * from the private emap we just destroyed.  Invalidate the cache so
	 * the next test's fresh emap doesn't follow stale entries.
	 */
	rtree_ctx_data_init(tsd_rtree_ctx(tsd_fetch()));
	free(data);
}

static edata_t *
pac_alloc_expect(pac_test_data_t *data, size_t size, bool guarded) {
	bool deferred_work_generated = false;
	edata_t *edata = pac_alloc(tsdn_fetch(), &data->shard.pac, size, PAGE,
	    /* zero */ false, guarded, /* frequent_reuse */ false,
	    &deferred_work_generated);
	expect_ptr_not_null(edata, "Unexpected pac_alloc failure");
	expect_zu_eq(size, edata_size_get(edata), "Unexpected allocation size");
	return edata;
}

TEST_BEGIN(test_pac_dirty_muzzy_alloc_priority) {
	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;
	size_t alloc_size = HUGEPAGE;
	size_t alloc_npages = alloc_size >> LG_PAGE;

	edata_t *muzzy = pac_alloc_expect(
	    data, alloc_size, /* guarded */ false);
	void *muzzy_addr = edata_base_get(muzzy);
	edata_t *dirty = pac_alloc_expect(
	    data, alloc_size, /* guarded */ false);
	void *dirty_addr = edata_base_get(dirty);

	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, muzzy, &deferred_work_generated);
	pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ false);
	expect_zu_eq(alloc_npages, ecache_npages_get(&pac->ecache_muzzy),
	    "Expected one muzzy page after dirty decay");

	deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, dirty, &deferred_work_generated);
	expect_zu_eq(alloc_npages, ecache_npages_get(&pac->ecache_dirty),
	    "Expected one dirty page");

	edata_t *from_dirty = pac_alloc_expect(
	    data, alloc_size, /* guarded */ false);
	expect_ptr_eq(dirty_addr, edata_base_get(from_dirty),
	    "Dirty cache should be preferred over muzzy");

	edata_t *from_muzzy = pac_alloc_expect(
	    data, alloc_size, /* guarded */ false);
	expect_ptr_eq(muzzy_addr, edata_base_get(from_muzzy),
	    "Muzzy cache should be used after dirty cache");

	deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, from_dirty, &deferred_work_generated);
	deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, from_muzzy, &deferred_work_generated);

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_batched_grow_caches_trailing_dirty) {
	test_skip_if(!sz_large_size_classes_disabled()
	    || !(maps_coalesce || opt_retain));

	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;

	size_t size = HUGEPAGE + PAGE;
	size_t batched_size = sz_s2u_compute_using_delta(size);
	size_t next_hugepage_size = HUGEPAGE_CEILING(size);
	if (batched_size > next_hugepage_size) {
		batched_size = next_hugepage_size;
	}
	assert_zu_gt(batched_size, size,
	    "Test size should exercise batched retained growth");

	size_t dirty_before = ecache_npages_get(&pac->ecache_dirty);
	edata_t *large = pac_alloc_expect(data, size, /* guarded */ false);
	expect_zu_eq(dirty_before + ((batched_size - size) >> LG_PAGE),
	    ecache_npages_get(&pac->ecache_dirty),
	    "Batched grow should cache the trailing dirty extent");
	if (config_stats) {
		expect_zu_ge(pac_mapped(pac), batched_size,
		    "Mapped stats should include the full batched grow");
	}

	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, large, &deferred_work_generated);

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_deferred_work_signals) {
	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;

	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
	expect_true(deferred_work_generated,
	    "Non-pinned dalloc should request deferred work");

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_decay_dirty_to_muzzy_via_purge_lazy) {
	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;

	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);

	unsigned purge_lazy_before = pac_test_purge_lazy_calls;
	pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ false);
	expect_zu_eq(0, ecache_npages_get(&pac->ecache_dirty),
	    "Dirty decay should remove dirty pages");
	expect_zu_eq(1, ecache_npages_get(&pac->ecache_muzzy),
	    "Successful lazy purge should move dirty pages to muzzy");
	expect_u_gt(pac_test_purge_lazy_calls, purge_lazy_before,
	    "Dirty-to-muzzy decay should call purge_lazy");

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_decay_retains_when_dalloc_fails) {
	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;

	pac_test_dalloc_fail = true;
	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
	size_t retained_before = ecache_npages_get(&pac->ecache_retained);
	pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ true);
	expect_zu_gt(ecache_npages_get(&pac->ecache_retained), retained_before,
	    "Fully decayed dirty pages should be retained when dalloc fails");
	expect_u_gt(pac_test_dalloc_calls, 0,
	    "Fully decayed dirty pages should attempt dalloc first");

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_non_pinned_dalloc_signals_deferred_work) {
	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;

	size_t normal_size = 3 * HUGEPAGE + PAGE;
	edata_t *normal = pac_alloc_expect(data, normal_size,
	    /* guarded */ false);
	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, normal, &deferred_work_generated);
	expect_true(deferred_work_generated,
	    "Non-pinned dalloc should request deferred work");

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_non_pinned_shrink_signals_deferred_work) {
	test_skip_if(!maps_coalesce);

	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	pac_t *pac = &data->shard.pac;

	size_t normal_size = 3 * HUGEPAGE + PAGE;
	size_t normal_shrink_size = 3 * HUGEPAGE;
	edata_t *normal = pac_alloc_expect(data, normal_size,
	    /* guarded */ false);
	bool deferred_work_generated = false;
	expect_false(pac_shrink(tsdn_fetch(), pac, normal, normal_size,
	    normal_shrink_size, &deferred_work_generated),
	    "Unexpected non-pinned shrink failure");
	expect_true(deferred_work_generated,
	    "Non-pinned shrink should request deferred work");
	pac_dalloc(tsdn_fetch(), pac, normal, &deferred_work_generated);

	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_large_guarded_dalloc_unguards_before_caching) {
	pac_test_hooks_reset();
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, -1, -1);
	edata_t *guarded = pac_alloc_expect(data, SC_LARGE_MINCLASS,
	    /* guarded */ true);
	expect_true(edata_guarded_get(guarded),
	    "Guarded allocation should set the guarded bit");
	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), &data->shard.pac, guarded,
	    &deferred_work_generated);
	expect_true(deferred_work_generated,
	    "Guarded dalloc should still request deferred work");
	expect_false(edata_guarded_get(guarded),
	    "Large guarded dalloc should unguard before caching");
	pac_test_data_destroy(data);
}
TEST_END

TEST_BEGIN(test_pac_deferred_never_does_not_purge) {
	/*
	 * PAC_PURGE_NEVER is the eagerness the application free path selects
	 * when a background thread is enabled, meaning current thread does not
	 * purge.  It is only reachable through the exposed pac_decay_deferred
	 * primitive (the production entry pac_do_deferred_work picks eagerness
	 * itself), so this is a deliberate internal test of that critical,
	 * stable edge.
	 */
	pac_test_hooks_reset();
	/*
	 * Finite dirty decay so a purge is gated by eagerness, not decay_ms<=0.
	 */
	pac_test_data_t *data = pac_test_data_init(
	    /* custom_hooks */ true, /* dirty_decay_ms */ 100 * 1000, -1);
	pac_t *pac = &data->shard.pac;

	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
	size_t ndirty_before_dalloc = ecache_npages_get(&pac->ecache_dirty);

	/* Dalloc caches the extent as dirty and requests deferred work. */
	bool deferred_work_generated = false;
	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
	expect_true(deferred_work_generated,
	    "Non-pinned dalloc should request deferred work");
	size_t ndirty_after_dalloc = ecache_npages_get(&pac->ecache_dirty);
	expect_zu_gt(ndirty_after_dalloc, ndirty_before_dalloc,
	    "Dalloc should add dirty pages");

	/* PAC_PURGE_NEVER must leave the dirty pages untouched. */
	pac_deferred_work_result_t result;
	pac_decay_deferred(tsdn_fetch(), pac, PAC_PURGE_NEVER, &result);
	expect_zu_eq(ecache_npages_get(&pac->ecache_dirty), ndirty_after_dalloc,
	    "PAC_PURGE_NEVER must not purge any dirty pages");

	pac_test_data_destroy(data);
}
TEST_END

int
main(void) {
	return test_no_reentrancy(test_pac_dirty_muzzy_alloc_priority,
	    test_pac_batched_grow_caches_trailing_dirty,
	    test_pac_deferred_work_signals,
	    test_pac_decay_dirty_to_muzzy_via_purge_lazy,
	    test_pac_decay_retains_when_dalloc_fails,
	    test_pac_non_pinned_dalloc_signals_deferred_work,
	    test_pac_non_pinned_shrink_signals_deferred_work,
	    test_pac_large_guarded_dalloc_unguards_before_caching,
	    test_pac_deferred_never_does_not_purge);
}
