#include "test/jemalloc_test.h"

/* Test config (set in reset_test_config) */
#define ALLOC_ITERATIONS_IN_THRESHOLD 10
uint64_t threshold_bytes = 0;
uint64_t chunk_size = 0;

/* Test globals for calblack */
uint64_t hook_calls = 0;
uint64_t last_peak = 0;
uint64_t last_alloc = 0;
uint64_t alloc_baseline = 0;

void
mock_prof_threshold_hook(uint64_t alloc, uint64_t dealloc, uint64_t peak) {
	hook_calls++;
	last_peak = peak;
	last_alloc = alloc;
}

/* Need the do_write flag because NULL is a valid to_write value. */
static void
read_write_prof_threshold_hook(prof_threshold_hook_t *to_read, bool do_write,
    prof_threshold_hook_t to_write) {
	size_t hook_sz = sizeof(prof_threshold_hook_t);
	expect_d_eq(
	    mallctl("experimental.hooks.prof_threshold", (void *)to_read,
	        &hook_sz, do_write ? &to_write : NULL, hook_sz),
	    0, "Unexpected prof_threshold_hook mallctl failure");
}

static void
write_prof_threshold_hook(prof_threshold_hook_t new_hook) {
	read_write_prof_threshold_hook(NULL, true, new_hook);
}

static prof_threshold_hook_t
read_prof_threshold_hook() {
	prof_threshold_hook_t hook;
	read_write_prof_threshold_hook(&hook, false, NULL);
	return hook;
}

static void
reset_test_config() {
	hook_calls = 0;
	last_peak = 0;
	alloc_baseline = last_alloc; /* We run the test multiple times */
	last_alloc = 0;
	threshold_bytes = 1 << opt_experimental_lg_prof_threshold;
	chunk_size = threshold_bytes / ALLOC_ITERATIONS_IN_THRESHOLD;
}

static void
expect_threshold_calls(int calls) {
	expect_u64_eq(
	    hook_calls, calls, "Hook called the right amount of times");
	expect_u64_lt(
	    last_peak, chunk_size * 2, "We allocate chunk_size at a time");
	expect_u64_ge(
	    last_alloc, threshold_bytes * calls + alloc_baseline, "Crosses");
}

static void
allocate_chunks(int chunks) {
	for (int i = 0; i < chunks; i++) {
		void *p = mallocx((size_t)chunk_size, 0);
		expect_ptr_not_null(p, "Failed to allocate");
		free(p);
	}
}

TEST_BEGIN(test_prof_threshold_hook) {
	test_skip_if(!config_stats);

	/* Test setting and reading the hook (both value and null) */
	write_prof_threshold_hook(mock_prof_threshold_hook);
	expect_ptr_eq(read_prof_threshold_hook(), mock_prof_threshold_hook,
	    "Unexpected hook");

	write_prof_threshold_hook(NULL);
	expect_ptr_null(read_prof_threshold_hook(), "Hook was erased");

	/* Reset everything before the test */
	reset_test_config();
	write_prof_threshold_hook(mock_prof_threshold_hook);

	int err = mallctl("thread.peak.reset", NULL, NULL, NULL, 0);
	expect_d_eq(err, 0, "Peak reset failed");

	/* Note that since we run this test multiple times and we don't reset
	   the allocation counter, each time we offset the callback by the
	   amount we allocate over the threshold. */

	/* A simple small allocation is not enough to trigger the callback */
	allocate_chunks(1);
	expect_u64_eq(hook_calls, 0, "Hook not called yet");

	/* Enough allocations to trigger the callback */
	allocate_chunks(ALLOC_ITERATIONS_IN_THRESHOLD);
	expect_threshold_calls(1);

	/* Enough allocations to trigger the callback again */
	allocate_chunks(ALLOC_ITERATIONS_IN_THRESHOLD);
	expect_threshold_calls(2);
}
TEST_END

int
main(void) {
	return test(test_prof_threshold_hook);
}
