/*
 * Copyright (c) 2010-2011 Apple Inc. All rights reserved.
 *
 * @APPLE_APACHE_LICENSE_HEADER_START@
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @APPLE_APACHE_LICENSE_HEADER_END@
 */

#include <sys/event.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
#include <unistd.h>
#endif
#ifdef __APPLE__
#include <libkern/OSAtomic.h>
#endif
#include <assert.h>
#ifdef __ANDROID__
#include <linux/sysctl.h>
#else
#if !defined(__linux__)
#include <sys/sysctl.h>
#endif
#endif /* __ANDROID__ */
#include <stdarg.h>
#include <time.h>

#include <dispatch/dispatch.h>
#include <dispatch/private.h>

#include <bsdtests.h>
#include "dispatch_test.h"

#if defined(DISPATCH_SOURCE_TYPE_VM) && defined(NOTE_VM_PRESSURE)

#if TARGET_OS_EMBEDDED
#define ALLOC_SIZE ((size_t)(1024*1024*1ul))	// 1MB
#define NOTIFICATIONS 1
#else
#define ALLOC_SIZE ((size_t)(1024*1024*20ul))	// 20MB
#define NOTIFICATIONS 2
#endif
#define pg2mb(p) ((p) * ALLOC_SIZE/(1024*1024))
#ifdef __LP64__
#define MAXMEM ((size_t)SIZE_MAX)
#else
#define MAXMEM ((size_t)(3200ul*1024*1024))		// 3200MB
#endif

static char **pages;
static volatile int32_t handler_call_count;
static volatile int32_t page_count;
static int32_t max_page_count;
static dispatch_source_t vm_source;
static dispatch_queue_t vm_queue;
static time_t initial;
static int interval = 16;

#define log_msg(msg, ...) \
do { \
	fprintf(stderr, "[%2ds] " msg, (int)(time(NULL) - initial), ##__VA_ARGS__);\
} while (0)

static bool
dispatch_test_check_evfilt_vm(void)
{
	int kq = kqueue();
	assert(kq != -1);
	struct kevent ke = {
		.filter = EVFILT_VM,
		.flags = EV_ADD|EV_ENABLE|EV_RECEIPT,
		.fflags = NOTE_VM_PRESSURE,
	};
	int r = kevent(kq, &ke, 1, &ke, 1, NULL);
	close(kq);
	return !(r > 0 && ke.flags & EV_ERROR && ke.data == ENOTSUP);
}

static void
cleanup(void)
{
	dispatch_source_cancel(vm_source);
	dispatch_release(vm_source);
	dispatch_release(vm_queue);

	int32_t pc = 0, i;
	for (i = 0; i < max_page_count; ++i) {
	   if (pages[i]) {
		   pc++;
		   free(pages[i]);
	   }
	}
	if (pc) {
		log_msg("Freed %ldMB\n", pg2mb(pc));
	}
	free(pages);
	test_stop();
}

int
main(void)
{
	dispatch_test_start("Dispatch VM Pressure test"); // rdar://problem/7000945
	if (!dispatch_test_check_evfilt_vm()) {
		test_skip("EVFILT_VM not supported");
		test_stop();
		return 0;
	}
	initial = time(NULL);
	uint64_t memsize;
#ifdef __linux__
	memsize = sysconf(_SC_PAGESIZE) * sysconf(_SC_PHYS_PAGES);
#else
	size_t s = sizeof(memsize);
	int rc = sysctlbyname("hw.memsize", &memsize, &s, NULL, 0);
	assert(rc == 0);
#endif
	max_page_count = MIN(memsize, MAXMEM) / ALLOC_SIZE;
	pages = calloc(max_page_count, sizeof(char*));

	vm_queue = dispatch_queue_create("VM Pressure", NULL);
	vm_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VM, 0,
			DISPATCH_VM_PRESSURE, vm_queue);
	dispatch_source_set_event_handler(vm_source, ^{
		if (!page_count) {
			// Too much memory pressure already to start the test
			test_skip("Memory pressure at start of test");
			cleanup();
		}
		if (OSAtomicIncrement32Barrier(&handler_call_count) != NOTIFICATIONS) {
			log_msg("Ignoring vm pressure notification\n");
			interval = 1;
			return;
		}
		test_long("dispatch_source_get_data()",
				dispatch_source_get_data(vm_source), NOTE_VM_PRESSURE);
		int32_t i, pc = page_count + 1;
		for (i = 0; i < pc && pages[i]; ++i) {
				free(pages[i]);
				pages[i] = NULL;
		}
		log_msg("Freed %ldMB\n", pg2mb(i));
	});
	dispatch_resume(vm_source);
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC),
			dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
		while (handler_call_count < NOTIFICATIONS &&
				page_count < max_page_count) {
			void *p = valloc(ALLOC_SIZE);
			if (!p) {
				break;
			}
			bzero(p, ALLOC_SIZE);
			pages[page_count] = p;
			if (!(OSAtomicIncrement32Barrier(&page_count) % interval)) {
				log_msg("Allocated %ldMB\n", pg2mb(page_count));
				usleep(200000);
			}
		}
		if (page_count % interval) {
			log_msg("Allocated %ldMB\n", pg2mb(page_count));
		}
		if (handler_call_count < NOTIFICATIONS) {
			// Cannot allocate enough memory for test (e.g. on 32 bit)
			test_skip("Cannot allocate enough memory for test");
			dispatch_async(vm_queue, ^{cleanup();});
			return;
		}
		dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),
				vm_queue, ^{
			test_long_greater_than_or_equal("VM Pressure fired",
					handler_call_count, NOTIFICATIONS);
			test_long_less_than("VM Pressure stopped firing",
					handler_call_count, 4);
			cleanup();
		});
	});
	dispatch_main();
	return 0;
}
#else //DISPATCH_SOURCE_TYPE_VM
int
main(void)
{
	dispatch_test_start("Dispatch VM Pressure test"
			" - No DISPATCH_SOURCE_TYPE_VM");
	test_stop();
	return 0;
}
#endif //DISPATCH_SOURCE_TYPE_VM
